diff --git a/.agents/skills/blume/SKILL.md b/.agents/skills/blume/SKILL.md new file mode 100644 index 0000000000..52c6703e2b --- /dev/null +++ b/.agents/skills/blume/SKILL.md @@ -0,0 +1,73 @@ +--- +name: blume +description: Build and maintain documentation sites with Blume, the markdown-first docs framework on Astro and Vite. Use when working in a project that depends on `blume`, when scaffolding or configuring a docs site, writing Markdown/MDX content, tuning navigation/search/theming/SEO/AI features, running the `blume` CLI (init, dev, build, eject), or editing `blume.config.ts` and `meta.ts` files. +--- + +# Blume + +Blume is an open-source, **markdown-first** documentation framework built on Astro and Vite. Drop Markdown or MDX into a folder, run `blume dev`, and get a production-grade docs site — navigation, search, theming, Open Graph images, and a rich component library — with no app boilerplate to write or maintain. + +The core idea: **the framework _is_ the template.** There's no starter to clone and no project to own before you've written a word. The only thing you touch is your content. When you outgrow the defaults, you add configuration one file at a time — and you can `blume eject` to a plain Astro project the day you want full control. + +## What makes it different + +- **Fast by default** — Static HTML on Astro/Vite. The core theme ships no client framework JS so pages score well on Core Web Vitals out of the box. You opt into server features only when you need them. +- **AI-ready out of the box** — Emits `llms.txt`/`llms-full.txt`, serves any page's raw Markdown by appending `.md` to its URL, offers **Copy as Markdown** and **Open in chat** on every page, and can host an optional **Ask AI** assistant or an **MCP server** so coding agents read your docs directly. +- **Zero configuration — even the template** — A folder of docs is a complete project. Navigation is inferred from files, search works in dev and production with no hosted service, and theming is a handful of tokens. +- **Type-safe to the core** — `blume.config.ts` and every `meta.ts` are real TypeScript, validated by a schema and authored with `defineConfig` and `defineMeta`. Your editor autocompletes options and catches mistakes before a build. + +## Quickstart + +Blume needs **Node.js 22.12 or newer**. From an empty or existing project: + +```bash +npm i blume # install the package +blume init # scaffold: docs/index.mdx + blume.config.ts +blume dev # dev server with hot reload +blume build # static HTML to dist/, with a local search index +``` + +Blume works with any package manager and never requires you to set up Astro or Tailwind yourself. + +### Writing a page + +Every page is Markdown or MDX with a little frontmatter. The `title` and `description` render as the page heading and intro automatically; built-in components (callouts, cards, tabs, steps, and more) need **no imports**. + +```mdx +--- +title: Introduction +description: Welcome to my docs. +--- + +Welcome! Use **Markdown** and built-in components — no imports required: + +:::note +Blume ships callouts, cards, tabs, steps, and more. +::: +``` + +Navigation, search, and page metadata are inferred from your files as you add them. + +## What's included + +- **Components** — callouts, cards, steps, tabs, accordions, badges, file trees, and parameter tables, usable in MDX with no imports. +- **Local search** — Orama in dev and production; Pagefind is one flag away for large sites. No hosted index. +- **AI** — `llms.txt`, raw Markdown URLs, Copy as Markdown, Open in chat, an Ask AI assistant, and an MCP server endpoint served by the docs site itself. +- **Navigation** — inferred from files, refined with `meta.ts` or config. +- **SEO** — metadata, Open Graph images, RSS feeds, and JSON-LD. +- **Customization** — component overrides, React islands, custom pages, theme tokens, and a source-component registry via `blume add`. +- **Eject** — `blume eject` produces a standalone Astro project that still uses the `blume` package. + +## How it works + +The Blume CLI discovers your content, builds a content graph, and generates a hidden Astro project under `.blume/` that it drives for dev and build. The generated runtime is an implementation detail — you write Markdown, Blume handles the rest — until you choose to eject and own it. + +## Full documentation + +This is a high-level overview. For complete, authoritative docs — configuration reference, every CLI command and flag, component APIs, content authoring, navigation, search, SEO, AI features, theming, and deployment — read the bundled docs in the installed package: + +``` +node_modules/blume/docs +``` + +Start with `node_modules/blume/docs/index.mdx` (Introduction) and `node_modules/blume/docs/01-quickstart.mdx`, then browse the `configuration/`, `content/`, `reference/`, and `advanced/` sections for specifics. diff --git a/.agents/skills/supertokens-docs-proofreader/SKILL.md b/.agents/skills/supertokens-docs-proofreader/SKILL.md new file mode 100644 index 0000000000..bf86cf163a --- /dev/null +++ b/.agents/skills/supertokens-docs-proofreader/SKILL.md @@ -0,0 +1,145 @@ +--- +name: supertokens-docs-proofreader +description: Proofread and fact-check SuperTokens documentation page by page against released SuperTokens SDKs, the Core, API specifications, tests, and official repositories. Use when asked to audit documentation accuracy or freshness, validate one or more docs pages, correct stale or wrong SuperTokens guidance, review SDK-specific examples, or summarize evidence-backed documentation corrections. +--- + +# SuperTokens Docs Proofreader + +Audit user-facing documentation as both a technical editor and a SuperTokens implementation reviewer. Treat factual correctness as more important than stylistic polish. + +## Ground Rules + +- Verify claims from source. Do not rely on model memory or copy claims between docs pages. +- Document released behavior. Do not present code on an unreleased default branch as generally available. +- Validate each SDK tab independently. Similar APIs across Node.js, Python, Go, Java, PHP, .NET, frontend, mobile, and framework SDKs are not evidence of parity. +- Use the smallest evidence-backed correction. Preserve page structure, voice, MDX components, frontmatter, and unrelated local changes. +- Do not silently remove uncertain content. Record unresolved claims and the evidence needed to settle them. +- Never modify SDK or Core repositories during an audit. Read or fetch them without changing their worktrees. +- Do not create a branch, commit, or pull request unless the user asks. + +## Scope + +If the user names pages, audit those pages and the directly linked prerequisites needed to assess them. Otherwise: + +1. Locate the Blume content root from `blume.config.ts`; default to `docs/`. +2. Enumerate every user-facing `.md` and `.mdx` page in that root. Treat a page as its rendered output: trace imported components, shared snippets, data files, generators, and runtime-backed widgets that add technical claims. +3. Exclude authoring templates, generated artifacts, vendored content, and build output unless requested. +4. Include generated API/reference sections by validating their source specification or generator. Do not hand-edit generated output. +5. Process pages in navigation order when `meta.ts` provides one, then process remaining pages by path. + +For a repository-wide audit, maintain a ledger with one row per page. Track outcome (`unchanged` or `corrected`), completeness (`pending`, `complete`, or `blocked`), and generated content separately. A page is not complete until its rendered factual claims, code examples, links, and prose have been considered. Any unresolved material claim makes the page `blocked`, even when other claims were corrected. A material claim affects a user's implementation, security, compatibility, deployment, cost, or expected product behavior. + +## Workflow + +### 1. Establish Context + +1. Read repository instructions, contributor guidance, docs templates, and relevant `meta.ts` files. +2. Inspect the worktree before editing. Preserve all unrelated changes. +3. Read `references/validation-checklist.md` before starting the audit. +4. Identify whether the docs target latest releases, a versioned release, or unreleased behavior. Ask one short question only when the target cannot be inferred. +5. Create an audit workspace under `/tmp/opencode/supertokens-docs-proofreader//`. Keep the release manifest, claim evidence, and page ledger there until the final report is complete so the audit can be reviewed and resumed. + +### 2. Locate Authoritative Sources + +Prefer local sibling repositories under the common SuperTokens workspace when present. Discover repositories rather than assuming the list is complete. Typical sources include: + +- `supertokens-core` for Core behavior, storage, configuration, CDI implementation, and deployment behavior. +- Backend SDK repositories such as `supertokens-node`, `supertokens-python`, `supertokens-golang`, `supertokens-java`, `supertokens-php`, and `supertokens-dotnet`. +- Frontend and UI repositories such as `supertokens-web-js`, `supertokens-auth-react`, `supertokens-website`, and supported mobile SDK repositories. +- Integration repositories such as `supertokens-nestjs`, plugins, and plugin interfaces when a page documents them. +- This docs repository's `openapi/`, generators, examples, and code-block checks for generated references and documentation contracts. + +When a required repository is absent, use only the official `supertokens` GitHub organization, official package registries, and official SuperTokens release notes. Record the URL and revision used. + +For local Git repositories: + +1. Inspect remotes, current revision, default branch, tags, and worktree state. +2. Fetch release metadata when network access is available. For current documentation, freshness always matters. Do not checkout, pull, reset, clean, or alter files. +3. Inspect remote revisions with `git show`, `git grep`, or a temporary clone/worktree under `/tmp/opencode`. +4. Match claims to the latest relevant published package or binary, then map that artifact to its release tag and source commit. Use the default branch only for explicitly unreleased documentation or when release evidence proves the behavior shipped. + +Before confirming current behavior, create a release manifest for every relevant product and SDK. Record the registry or distribution channel, published version, release tag, source commit, retrieval date, and evidence URL. If the latest published state cannot be established, mark affected pages `blocked`. + +For cross-stack pages, also record the applicable Core, backend SDK, frontend SDK, CDI, and FDI versions as a compatibility tuple. Confirm compatibility from released SDK metadata, dependency constraints, compatibility tables, or tests. Do not combine each repository's newest release unless evidence shows that set interoperates. + +Use this evidence order: + +1. Released implementation, exported types, and tests at the relevant tag. +2. Versioned API specifications, generated references, and SDK documentation in the source repository. +3. Official changelogs, release notes, migration guides, and package metadata. +4. Official examples. +5. Existing prose documentation only as a lead, never as sole confirmation. + +Published artifacts define whether behavior is released; source and tests define how it behaves. If registry metadata, tags, release notes, specifications, implementation, or tests disagree, do not choose silently. Recheck version mapping, report the conflict, and mark the affected claim `blocked`. For public wire contracts, require agreement between the released contract and implementation or explicitly report the discrepancy. + +### 3. Audit Each Page + +Read the entire page, then inventory claims that can become stale: + +- API names, signatures, return values, exceptions, defaults, and deprecations. +- Recipe availability, feature support, SDK/framework compatibility, and version requirements. +- Core configuration, environment variables, ports, connection behavior, storage, and deployment steps. +- Authentication, session, token, cookie, anti-CSRF, account linking, multitenancy, MFA, and security behavior. +- Dashboard, managed service, licensing, and operational claims. +- Installation commands, package names, imports, routes, request/response examples, and code snippets. +- Internal links, prerequisites, sequencing, terminology, grammar, and ambiguous instructions. + +Trace every material claim to concrete evidence. Search by exact symbol, config key, route, error name, or behavior. For security-sensitive or surprising behavior, confirm with implementation and tests when available. + +Check examples against the exact SDK represented by their tab or label. Confirm imports, casing, argument order, async behavior, middleware ordering, framework version, and required initialization. Run targeted code-block tooling where practical. + +Mark the page on separate dimensions: + +- Outcome `unchanged`: no factual or editorial correction needed. +- Outcome `corrected`: one or more evidence-backed edits made. +- Completeness `complete`: every material claim was confirmed or corrected. +- Completeness `blocked`: at least one material claim could not be confirmed, including on an otherwise corrected page. +- Generated `yes`: source/generator checked instead of hand-editing output. This does not imply that the page is complete or unchanged. + +### 4. Edit + +1. Correct factual errors and stale instructions immediately when evidence is conclusive. +2. Fix grammar, clarity, and consistency without changing technical meaning or creating broad rewrite churn. +3. Update all variants of the same proven error when they are in scope, but do not infer that superficially similar text is wrong. +4. Preserve valid version-specific guidance and label it clearly when needed. +5. Update links or navigation metadata only when required by the correction. + +### 5. Verify + +Run the narrowest relevant checks first, then repository-level checks when the audit scope warrants them: + +```sh +npm run lint:code-blocks -- +npm run write-code-blocks -- +npm run lint:vale +npm run lint:prettier:check +npm run validate +npm run typecheck +npm run build +``` + +After extracting snippets, compile or type-check every supported language represented by an affected example: + +```sh +npm run check-code-blocks +``` + +Record unsupported, intentionally partial, or non-runnable examples as residual risk. Mark the page `blocked` when its correctness depends on an example that cannot otherwise be confirmed from released source and tests. Fix failures caused by the edits. Report pre-existing or environment-related failures separately. + +Review `git diff --check` and the final diff. Ensure every content change has evidence and no unrelated changes were included. + +## Final Report + +Lead with the result and include: + +1. **Coverage:** pages considered, unchanged, corrected, complete, blocked, generated, and intentionally excluded. +2. **Corrections:** one row per changed page with the original problem, correction, and authoritative source including repository plus tag, commit, file, symbol, or URL. +3. **Editorial changes:** concise summary of non-factual proofreading edits. +4. **Validation:** commands run and results. +5. **Unresolved:** claims that remain unverified, why, and what evidence is missing. + +Summarize semantic diffs, not line-by-line edits. If no changes are needed, state that clearly and still report coverage, sources checked, validation performed, and residual risk. + +## Resource + +- `references/validation-checklist.md`: page checklist, evidence rules, and audit ledger format. diff --git a/.agents/skills/supertokens-docs-proofreader/references/validation-checklist.md b/.agents/skills/supertokens-docs-proofreader/references/validation-checklist.md new file mode 100644 index 0000000000..06555cce94 --- /dev/null +++ b/.agents/skills/supertokens-docs-proofreader/references/validation-checklist.md @@ -0,0 +1,101 @@ +# Validation Checklist + +Use this checklist for every audited page. Adapt checks to the page; do not force irrelevant work. + +## Evidence Record + +Record enough detail for another reviewer to reproduce each conclusion: + +```text +Claim: +Verdict: confirmed | stale | wrong | ambiguous | unresolved +Product and version: +Source: repository, tag/commit, file, symbol/test, or official URL +Action: none | corrected | clarified | blocked +``` + +Do not cite a repository root or default branch alone. Prefer a release tag and exact file, exported symbol, test, specification operation, or changelog entry. + +Assign stable IDs within the audit, such as `docs/path/page.mdx#C01`, to material claims. Link each claim record from the page ledger. Store the ledger, claim records, release manifest, retrieval date, and audit scope under the audit workspace named in `SKILL.md`. Update them after each page and retain them through final review. + +## Page Checklist + +- Read the complete page, including frontmatter, imports, callouts, tabs, code groups, and linked prerequisites. +- Trace imported components, shared snippets, data sources, generators, and runtime-backed content that contribute to the rendered page. +- Identify the intended audience, SDK, framework, recipe, deployment model, and product version. +- Confirm feature names and terminology against current released exports and product language. +- Confirm package names, install commands, imports, initialization, API signatures, defaults, and return/error behavior. +- Confirm every language or SDK tab separately against that SDK's released source. +- Confirm Core claims against released Core implementation, configuration, tests, and CDI specifications. +- Confirm frontend behavior against the frontend SDK or UI implementation, not a backend SDK. +- Confirm routes and payloads against the relevant versioned CDI, FDI, or dashboard API contract. +- Confirm security statements from code and tests where possible; do not weaken warnings while editing. +- Confirm compatibility and minimum-version claims from release metadata or changelogs. +- Confirm managed-service, dashboard, licensing, and pricing claims using official current sources; mark them blocked if authoritative access is unavailable. +- Check that steps are complete, ordered, and explicit about prerequisites and side effects. +- Check code for syntax, imports, casing, async behavior, framework conventions, and middleware order. +- Check links and anchors. Prefer stable official links over branch-specific source links in page prose. +- Proofread for grammar, American spelling, active voice, concise language, defined acronyms, and consistent terms. +- Preserve deliberate versioned, legacy, and migration guidance rather than rewriting it as latest-only behavior. +- Record the page status and evidence before moving to the next page. +- Mark completeness `blocked` whenever any material claim remains unresolved, regardless of corrections made elsewhere on the page. + +## Release Check + +Before using default-branch code as evidence: + +1. Find the latest package release or repository release tag relevant to the docs. +2. Determine whether the symbol or behavior exists at that release. +3. Check changelogs and migration notes for deprecations or breaking changes. +4. Treat behavior found only after the release tag as unreleased unless official release evidence says otherwise. +5. For version-qualified docs, inspect the exact documented version instead of latest. + +For each relevant product or SDK, record: + +```text +Product: +Registry or distribution channel: +Published version: +Release tag: +Source commit: +Retrieved: +Evidence URL: +``` + +For pages spanning products, record a Core/backend/frontend/CDI/FDI compatibility tuple and the released evidence that proves it interoperates. Never construct the tuple from unrelated latest versions without compatibility evidence. + +If published artifacts, tags, specifications, implementation, or release notes conflict, verify the artifact-to-commit mapping. If the conflict remains, preserve the docs unless they are conclusively wrong, record the discrepancy, and mark the claim `blocked`. + +## Generated Content + +For generated references: + +1. Identify the checked-in source specification or generator. +2. Compare it with the released product contract. +3. Fix the source or generator, not generated output. +4. Regenerate using the repository command when available. +5. Report generated files separately from authored page corrections. + +## Audit Ledger + +Use a temporary ledger for multi-page audits: + +```markdown +| Page | Product/SDK | Outcome | Completeness | Generated | Claims | Notes | +| ------------------ | ----------- | --------- | ------------ | --------- | ------- | ---------- | +| docs/path/page.mdx | Node.js SDK | unchanged | complete | no | C01-C08 | No changes | +``` + +Keep the ledger outside the repository unless the user asks for a persistent report. For long audits, work in explicit batches and record the last completed page after every batch. Do not claim repository-wide completion if any in-scope page is missing from the ledger or has completeness other than `complete`. + +## Diff Summary + +For each changed page, report: + +```markdown +| Page | Problem | Correction | Evidence | +| -------------------- | ---------------------- | ------------------------------------- | ---------------------------------------------- | +| `docs/path/page.mdx` | Documented old default | Replaced it with the released default | `supertokens-core` vX.Y.Z, `path/file:setting` | +``` + +Group purely editorial corrections in one concise paragraph unless they alter interpretation. diff --git a/.agents/skills/supertokens-integration/SKILL.md b/.agents/skills/supertokens-integration/SKILL.md new file mode 100644 index 0000000000..56a39d48cc --- /dev/null +++ b/.agents/skills/supertokens-integration/SKILL.md @@ -0,0 +1,63 @@ +--- +name: supertokens-integration +description: Integrate, configure, troubleshoot, and review SuperTokens authentication in existing applications. Use when adding email/password, passwordless, social login, passkeys, MFA, email verification, enterprise login, sessions, roles, attack protection, or machine-to-machine authentication. +license: Apache-2.0 +metadata: + author: supertokens + version: "1.0" +--- + +# SuperTokens Integration + +Use this skill to make a correct, project-specific SuperTokens change. Do not start by copying a generic quickstart. Inspect the repository and existing authentication code first. + +## Workflow + +1. Identify the application stack, package manager, frontend framework, backend framework, SDK versions, router, deployment model, and existing authentication implementation. +2. Check whether the project uses the managed service or self-hosted Core. Confirm feature availability and SDK support before choosing an approach. +3. Determine the required recipes and ask focused questions for decisions that cannot be inferred: + - Authentication methods: email/password, passwordless, social, passkeys, or enterprise. + - UI: pre-built or custom. + - Passwordless contact method and flow type. + - Social providers and callback URLs. + - MFA factors, enforcement policy, and tenant-specific behavior. + - Email verification mode and delivery provider. + - Single-tenant or multi-tenant configuration. +4. Read the relevant current pages from the SuperTokens documentation. Prefer the documentation MCP server when available: + - `https://supertokens.com/docs/mcp` + - `https://supertokens.com/docs/llms.txt` + - Append `.md` to a documentation URL for agent-optimized Markdown. +5. Produce a short implementation plan covering frontend, backend, Core, environment variables, routes, middleware, and validation before editing. +6. Implement frontend and backend configuration together. Keep recipe configuration consistent between them. +7. Preserve project conventions. Do not replace an existing router, framework adapter, session strategy, or UI unless the task requires it. +8. Put connection URIs, API keys, provider secrets, email credentials, SMS credentials, and attack-protection secrets in environment variables. Never commit secrets or print them in logs. +9. Run the narrowest relevant tests first, then typechecks and the production build. Verify the actual authentication flow where possible. +10. Report changed files, required environment variables, unresolved decisions, validation commands, and results. + +## Recipe Guidance + +- Initialize the Session recipe wherever user authentication requires sessions. +- Email/password integrations commonly also need password reset and email verification decisions. +- Passwordless requires separate choices for contact method (`email`, `phone`, or both) and flow type (magic link, OTP, or both). Check email or SMS delivery configuration. +- Social login requires provider credentials, callback URLs, and server-side secret handling. +- Passkeys require supported backend SDKs, a correct origin and relying-party configuration, and HTTPS outside local development. +- MFA requires account linking and careful first-factor, second-factor, tenant, and email-verification configuration. Check managed-service and SDK limitations. +- Enterprise authentication requires tenant identification, tenant-specific enabled factors, and provider configuration per tenant. +- Machine-to-machine authentication should use the managed-service OAuth2 client-credentials flow when supported. Explain legacy-flow tradeoffs when it is not. +- User roles must be enforced on the backend. Frontend checks only control presentation and are not authorization. +- Attack Protection Suite setup must distinguish public frontend values from backend secret values and must preserve request ID propagation. + +## Validation Checklist + +- Frontend and backend use compatible recipe configurations. +- Authentication routes are mounted and reachable at the configured base path. +- Session middleware, cookies, CORS, and proxy settings work in the deployment environment. +- Protected backend routes reject missing, invalid, expired, and insufficiently authorized sessions. +- Sign-up, sign-in, sign-out, refresh, and the recipe-specific flow work as expected. +- Provider callbacks, password reset or verification links, and redirects use the correct domains. +- No credentials, API keys, or private provider configuration were added to tracked files. +- Relevant tests, typechecks, and build commands pass. + +## Troubleshooting + +When a flow fails, inspect the browser and server network requests, SDK and Core versions, configured domains, auth base paths, route mounting, CORS headers, cookie attributes, and Core connectivity before changing application logic. Compare the project against the relevant documentation page and report the first failing boundary. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..a2efa882a3 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +AI_GATEWAY_API_KEY= +POSTHOG_PROJECT_TOKEN= +POSTHOG_HOST= +POSTHOG_ASK_AI_SURVEY_ID= +POSTHOG_ASK_AI_RATING_QUESTION_ID= +POSTHOG_ASK_AI_REASON_QUESTION_ID= +POSTHOG_ASK_AI_COMMENT_QUESTION_ID= + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 65a65bcf79..bd2b6e6109 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,9 +9,10 @@ ## Checklist -- [ ] Algolia search needs to be updated? (If there is a new sub docs project, then yes) -- [ ] Sitemap needs to be updated? (If there is a new sub docs project, then yes) -- [ ] Checked for broken links? (Run `cd v3 && npm run build`) +- [ ] Ran `npm run build` +- [ ] Ran `npm run typecheck` +- [ ] Ran `npm test` +- [ ] Checked changed routes and redirects - [ ] Changes required to the demo apps corresponding to the docs? ## Remaining TODOs for this PR diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bd21edd52b..c4c908a819 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,19 +1,52 @@ -name: Deploy -run-name: Deploy to ${{ github.ref_name == 'master' && 'the production website' || 'a preview website' }} +name: Build on: + workflow_dispatch: + inputs: + preview_url: + description: Shared preview URL to smoke test after the build + type: string + required: false pull_request: paths: - "docs/**" - - "static/**" - - "src/**" + - "components/**" + - "islands/**" + - "lib/**" + - "pages/**" + - "openapi/**" + - "public/**" + - "e2e/**" + - "e2e-preview/**" + - "scripts/blume/**" + - "scripts/migration/**" + - "scripts/serve-e2e.mjs" + - "blume.config.ts" + - "components.ts" + - "microfrontends.jsonc" + - "theme.css" + - "tsconfig.json" + - "vercel.json" + - "playwright.config.ts" + - "playwright.preview.config.ts" + - ".github/workflows/deploy.yml" - "package-lock.json" + - "package.json" push: branches: - master jobs: + # Direct Vercel Git deployments must require this GitHub Build check when the repository-variable gate is enabled. + full-code-block-checks: + if: vars.RUN_FULL_CODE_BLOCK_CHECKS == 'true' && github.event_name != 'pull_request' + uses: ./.github/workflows/test.yml + with: + full: true + build: + needs: full-code-block-checks + if: always() && (needs.full-code-block-checks.result == 'success' || needs.full-code-block-checks.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,50 +54,48 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22.12" cache: "npm" cache-dependency-path: "package-lock.json" - name: Install Dependencies - run: npm install + run: npm ci + + - name: Validate generated routes + run: | + npm run routes:manifest + git diff --exit-code -- scripts/blume/route-manifest.json scripts/blume/openapi-redirects.mjs + + - name: Test redirect generation + run: npm test -- --run scripts/migration - - name: Build Docusaurus Site + - name: Build Blume site run: npm run build - - name: Upload Build Artifacts - uses: actions/upload-artifact@v4 - with: - name: docusaurus-build - path: build - retention-days: 1 + - name: Install Playwright + run: npx playwright install --with-deps chromium - trigger: - needs: build - runs-on: ubuntu-latest - steps: - - name: Set STAGE environment variable - shell: bash - run: | - if [[ "${{ github.ref_name }}" == "master" ]]; then - STAGE="production" - else - STAGE="preview" - fi + - name: Test documentation feedback regressions + run: >- + npx playwright test --workers=1 + e2e/theme-consistency.spec.ts + e2e/image-sizing.spec.ts + e2e/mobile-search-controls.spec.ts + e2e/quickstart-deep-links.spec.ts + e2e/table-of-contents.spec.ts + e2e/homepage-capabilities.spec.ts + e2e/legacy-redirects.spec.ts + e2e/custom-404.spec.ts - echo "STAGE=$STAGE" >> $GITHUB_ENV + - name: Test shared preview + if: github.event_name == 'workflow_dispatch' && inputs.preview_url != '' + env: + MFE_PREVIEW_URL: ${{ inputs.preview_url }} + run: npm run test:preview-smoke - - name: Trigger the backend website CI - uses: actions/github-script@v7 + - name: Upload Build Artifacts + uses: actions/upload-artifact@v4 with: - github-token: ${{ secrets.BACKEND_WEBSITE_TOKEN }} - script: | - github.rest.actions.createWorkflowDispatch({ - owner: 'supertokens', - repo: 'supertokens-backend-website', - workflow_id: 'release-documentation-changes.yml', - ref: 'master', - inputs: { - "run-id": `${{ github.run_id }}`, - "stage": "${{ env.STAGE }}", - } - }) + name: blume-build + path: dist + retention-days: 1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index caa10dd9b8..2de561b807 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,12 +4,13 @@ on: workflow_dispatch: pull_request: paths: - - "docs/**" - - "src/**" - - "*.md" + - "docs/**/*.md" + - "docs/**/*.mdx" + - "islands/**/*.tsx" + - "CONTRIBUTING.md" - ".vale.ini" - ".vale/**" - - "package-lock.json" + - ".github/workflows/lint.yml" jobs: vale-lint: @@ -24,21 +25,44 @@ jobs: id: changed-doc-files uses: tj-actions/changed-files@v46.0.3 with: - files: | - **/*.md - **/*.mdx - src/**/*.tsx - .vale.ini - .vale/** + files_yaml: | + content: + - "docs/**/*.md" + - "docs/**/*.mdx" + - "islands/**/*.tsx" + - "CONTRIBUTING.md" + config: + - ".vale.ini" + - ".vale/**" + - ".github/workflows/lint.yml" + matrix: true - - name: Run vale + - name: Run Vale on changed content uses: errata-ai/vale-action@v2.1.1 - if: steps.changed-doc-files.outputs.any_changed == 'true' + if: >- + github.event_name != 'workflow_dispatch' && + steps.changed-doc-files.outputs.content_any_changed == 'true' && + steps.changed-doc-files.outputs.config_any_modified != 'true' with: - files: | - docs - src - CONTRIBUTING.md + files: ${{ steps.changed-doc-files.outputs.content_all_changed_files }} + vale_flags: "--minAlertLevel=warning" + version: 3.20.0 + reporter: local + filter_mode: nofilter + fail_on_error: true + + - name: Run full Vale validation + uses: errata-ai/vale-action@v2.1.1 + if: >- + github.event_name == 'workflow_dispatch' || + steps.changed-doc-files.outputs.config_any_modified == 'true' + with: + files: '["docs", "components", "islands", "CONTRIBUTING.md"]' + version: 3.20.0 + # PR reporters fetch GitHub's size-limited diff even with nofilter. + reporter: local + filter_mode: nofilter + fail_on_error: true # format-code: # runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54fe58eaa0..ab0418eec6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,48 +1,235 @@ name: Test +permissions: + contents: read + on: workflow_dispatch: + inputs: + full: + description: Check all documentation code blocks + type: boolean + default: true + workflow_call: + inputs: + full: + description: Check all documentation code blocks + type: boolean + default: false pull_request: paths: - - "docs/**" + - "docs/**/*.md" + - "docs/**/*.mdx" + - "scripts/format-code-blocks.ts" + - "scripts/write-code-blocks.ts" + - "scripts/check-code-blocks.*" + - "scripts/code-blocks/**" + - "scripts/code-type-checking/**" + - "lib/api-request-snippets.ts" + - "lib/api-request-snippets.test.ts" + - "components/ApiRequestSnippet.astro" + - "openapi/**" + - "package.json" + - "package-lock.json" + - "vitest.config.ts" + - "tsconfig.json" + - ".github/workflows/**" jobs: + changes: + runs-on: ubuntu-latest + outputs: + full: ${{ steps.changes.outputs.full }} + docs: ${{ steps.changes.outputs.docs }} + has-docs: ${{ steps.changes.outputs.has-docs }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed documentation + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + REQUEST_FULL: ${{ inputs.full }} + shell: bash + run: | + full=false + : > /tmp/changed-docs + + if [[ "$REQUEST_FULL" == "true" ]]; then + full=true + elif [[ -n "$BASE_SHA" ]]; then + while IFS= read -r -d '' file; do + case "$file" in + docs/*.md|docs/*.mdx) + if [[ -f "$file" ]]; then + printf '%s\0' "$file" >> /tmp/changed-docs + fi + ;; + scripts/format-code-blocks.ts|scripts/write-code-blocks.ts|scripts/check-code-blocks.*|scripts/code-blocks/*|scripts/code-type-checking/*|lib/api-request-snippets.ts|lib/api-request-snippets.test.ts|components/ApiRequestSnippet.astro|openapi/*|package.json|package-lock.json|vitest.config.ts|tsconfig.json|.github/workflows/*) + full=true + ;; + esac + done < <(git diff --name-only -z "$BASE_SHA"...HEAD) + fi + + docs=$(jq -Rsc 'split("\u0000") | map(select(length > 0))' /tmp/changed-docs) + docs_count=$(jq 'length' <<< "$docs") + if (( docs_count > 100 || ${#docs} > 50000 )); then + full=true + docs='[]' + fi + + if [[ "$docs" == "[]" ]]; then + has_docs=false + else + has_docs=true + fi + + echo "full=$full" >> "$GITHUB_OUTPUT" + echo "docs=$docs" >> "$GITHUB_OUTPUT" + echo "has-docs=$has_docs" >> "$GITHUB_OUTPUT" + + infrastructure: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22.23.2" + + - name: Pin npm + run: npm install --global npm@11.17.0 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - name: Install Dependencies + run: npm ci + + - name: Test Code Snippet Infrastructure + run: npm test -- --run scripts/code-blocks lib/api-request-snippets.test.ts + + lint-code-blocks: + needs: changes + if: needs.changes.outputs.full == 'true' || needs.changes.outputs.has-docs == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22.23.2" + + - name: Pin npm + run: npm install --global npm@11.17.0 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - name: Install Dependencies + run: npm ci + + - name: Lint Code Snippets + env: + FULL: ${{ needs.changes.outputs.full }} + CHANGED_DOCS: ${{ needs.changes.outputs.docs }} + shell: bash + run: | + if [[ "$FULL" == "true" ]]; then + npm run lint:code-blocks + else + mapfile -d '' files < <(jq -j '.[] + "\u0000"' <<< "$CHANGED_DOCS") + npm run lint:code-blocks -- "${files[@]}" + fi + write-code-blocks: + needs: changes + if: needs.changes.outputs.full == 'true' || needs.changes.outputs.has-docs == 'true' runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.snippets.outputs.matrix }} + has-snippets: ${{ steps.snippets.outputs.has-snippets }} steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22.23.2" + + - name: Pin npm + run: npm install --global npm@11.17.0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 - name: Install Dependencies - run: bun install + run: npm ci - name: Write Code Snippets - run: bun run write-code-blocks + env: + FULL: ${{ needs.changes.outputs.full }} + CHANGED_DOCS: ${{ needs.changes.outputs.docs }} + shell: bash + run: | + if [[ "$FULL" == "true" ]]; then + npm run write-code-blocks + else + mapfile -d '' files < <(jq -j '.[] + "\u0000"' <<< "$CHANGED_DOCS") + npm run write-code-blocks -- "${files[@]}" + fi - - name: Upload Code Type Checking Scripts + - name: Build language matrix + id: snippets + shell: bash + run: | + languages=() + for language in javascript go python swift php java csharp; do + directory="scripts/code-type-checking/$language/snippets" + if [[ -d "$directory" ]] && find "$directory" -type f -print -quit | grep -q .; then + languages+=("$language") + fi + done + + matrix=$(printf '%s\n' "${languages[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | {language: .}') + echo "matrix=$matrix" >> "$GITHUB_OUTPUT" + if (( ${#languages[@]} > 0 )); then + echo "has-snippets=true" >> "$GITHUB_OUTPUT" + else + echo "has-snippets=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upload Code Type Checking Snippets + if: steps.snippets.outputs.has-snippets == 'true' uses: actions/upload-artifact@v4 with: name: code-blocks - path: scripts/code-type-checking + path: | + scripts/code-type-checking/javascript/snippets + scripts/code-type-checking/go/snippets + scripts/code-type-checking/python/snippets + scripts/code-type-checking/swift/snippets + scripts/code-type-checking/php/snippets + scripts/code-type-checking/java/snippets + scripts/code-type-checking/csharp/snippets + if-no-files-found: error retention-days: 1 code-type-checking: needs: write-code-blocks + if: needs.write-code-blocks.outputs.has-snippets == 'true' runs-on: ubuntu-latest + timeout-minutes: 45 strategy: - matrix: - language: - - javascript - - go - - python - - swift - - php - - java - - csharp - # TODO: Check kotlin issues - # - kotlin + matrix: ${{ fromJSON(needs.write-code-blocks.outputs.matrix) }} steps: + - uses: actions/checkout@v4 + - name: Download Code Blocks uses: actions/download-artifact@v4 with: @@ -52,5 +239,9 @@ jobs: - uses: docker/setup-buildx-action@v3 - name: Check code snippets for ${{ matrix.language }} - working-directory: ./scripts/code-type-checking/${{ matrix.language }} - run: docker build -t code-check-${{ matrix.language }} . + uses: docker/build-push-action@v6 + with: + context: scripts/code-type-checking/${{ matrix.language }} + push: false + cache-from: type=gha,scope=code-type-checking-${{ matrix.language }} + cache-to: type=gha,mode=max,scope=code-type-checking-${{ matrix.language }} diff --git a/.gitignore b/.gitignore index f0e4f6138d..d994826946 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,15 @@ # Production /build +/dist +/.astro +/.blume +/.vercel +/playwright-report +/test-results +/.playwright-mcp # Generated files -.docusaurus .cache-loader # Editor config @@ -42,3 +48,5 @@ scripts/code-type-checking/dart/snippets/* tmp/* .opencode/* +.blume-verify/ +.vercel/ diff --git a/.vale.ini b/.vale.ini index 9e015c52a9..bb5adeb116 100644 --- a/.vale.ini +++ b/.vale.ini @@ -1,6 +1,6 @@ # Global Config StylesPath = .vale -MinAlertLevel = suggestion +MinAlertLevel = error Vocab = Authentication @@ -13,5 +13,14 @@ BasedOnStyles = SuperTokens, SuperTokensErrors [*.tsx] BasedOnStyles = SuperTokensErrors +[docs/references/backend-sdks/supertokens-nodejs/*.{md,mdx}] +BasedOnStyles = SuperTokensErrors +[docs/references/frontend-sdks/supertokens-auth-react/*.{md,mdx}] +BasedOnStyles = SuperTokensErrors +[docs/references/frontend-sdks/supertokens-web-js/*.{md,mdx}] +BasedOnStyles = SuperTokensErrors + +[docs/references/plugins/*.{md,mdx}] +BasedOnStyles = SuperTokensErrors diff --git a/.vale/SuperTokens/abbreviations.yml b/.vale/SuperTokens/abbreviations.yml index 85d9dbe99f..54b5b019b9 100644 --- a/.vale/SuperTokens/abbreviations.yml +++ b/.vale/SuperTokens/abbreviations.yml @@ -1,6 +1,6 @@ extends: substitution message: "Use '%s' instead of abbreviations like '%s'." -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#2-consistency +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#2-consistency ignorecase: true level: warning nonword: true diff --git a/.vale/SuperTokens/acronyms.yml b/.vale/SuperTokens/acronyms.yml index e5d39b9f98..f37b3951b1 100644 --- a/.vale/SuperTokens/acronyms.yml +++ b/.vale/SuperTokens/acronyms.yml @@ -25,7 +25,6 @@ exceptions: - DOM - GET - PKCE - - POST - SSO - CORS - UX @@ -51,7 +50,6 @@ exceptions: - POST - PUT - DELETE - - GET - REST - AWS - JWKS diff --git a/.vale/SuperTokens/adjectives.yml b/.vale/SuperTokens/adjectives.yml index 8e54ed1524..fc037cc7be 100644 --- a/.vale/SuperTokens/adjectives.yml +++ b/.vale/SuperTokens/adjectives.yml @@ -1,10 +1,8 @@ extends: existence message: "Remove '%s' if it's not important to the meaning of the statement." -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#1-clarity +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#1-clarity ignorecase: true -level: warning -action: - name: remove +level: suggestion tokens: - abnormally - absentmindedly diff --git a/.vale/SuperTokens/american-spelling.yml b/.vale/SuperTokens/american-spelling.yml index 69c9e591b0..941b5e8c56 100644 --- a/.vale/SuperTokens/american-spelling.yml +++ b/.vale/SuperTokens/american-spelling.yml @@ -1,55 +1,30 @@ -extends: existence -message: "In general, use American spelling instead of '%s'." -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#2-consistency +extends: substitution +message: "Use American spelling: '%s' instead of '%s'." +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#2-consistency ignorecase: true level: warning -tokens: - - '(?:\w+)nised' - - '(?:\w+)ise' - - '(?:\w+)logue' - - '(?:\w+)lour' - - '(?:\w+)lyse' - - '[a-zA-Z]{2,}our(?:\b|s|ed|ing)' -exceptions: - # *ise endings - - (?:A|a)dvertise - - (?:A|a)dvise - - (?:A|a)ppraise - - (?:A|a)pprise - - (?:A|a)rise - - (?:C|c)hastise - - (?:C|c)ircumcise - - (?:C|c)lockwise - - (?:C|c)omprise - - (?:C|c)ompromise - - (?:C|c)oncise - - (?:C|c)ounterclockwise - - (?:D|d)emise - - (?:D|d)espise - - (?:D|d)evise - - (?:D|d)isguise - - (?:E|e)nterprise - - (?:E|e)xcise - - (?:E|e)xercise - - (?:E|e)xpertise - - (?:F|f)ranchise - - (?:I|i)mprecise - - (?:I|i)mprovise - - (?:I|i)ncise - - (?:L|l)ikewise - - (?:M|m)erchandise - - (?:N|n)oise - - (?:O|o)therwise - - (?:P|p)aradise - - (?:P|p)oise - - (?:P|p)raise - - (?:P|p)recise - - (?:P|p)remise - - (?:P|p)romise - - (?:R|r)evise - - (?:R|r)ise - - (?:S|s)upervise - - (?:S|s)urmise - - (?:S|s)urprise - - (?:T|t)elevise - - (?:W|w)ise +scope: text +swap: + authorisation: authorization + behaviour: behavior + behaviours: behaviors + catalogue: catalog + colour: color + colours: colors + customise: customize + customised: customized + customisation: customization + initialise: initialize + initialised: initialized + initialisation: initialization + licence: license + organisation: organization + organisations: organizations + recognise: recognize + recognised: recognized + serialise: serialize + serialised: serialized + synchronise: synchronize + synchronised: synchronized + utilise: use + utilised: used diff --git a/.vale/SuperTokens/clarity.yml b/.vale/SuperTokens/clarity.yml index 5eb208b908..b5efa8f930 100644 --- a/.vale/SuperTokens/clarity.yml +++ b/.vale/SuperTokens/clarity.yml @@ -1,6 +1,6 @@ extends: substitution message: "Use concise language. Consider using '%s' instead of '%s'." -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#4-conciseness +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#4-conciseness level: suggestion scope: text diff --git a/.vale/SuperTokens/gender.yml b/.vale/SuperTokens/gender.yml index f06d0f9cc5..c2cf2fa3d4 100644 --- a/.vale/SuperTokens/gender.yml +++ b/.vale/SuperTokens/gender.yml @@ -1,13 +1,8 @@ extends: existence message: "Use a gender-neutral pronoun instead of '%s'." -level: error +level: suggestion ignorecase: true tokens: - he/she - s/he - \(s\)he - - \bhe\b - - \bhim\b - - \bhis\b - - \bshe\b - - \bher\b diff --git a/.vale/SuperTokens/headings.yml b/.vale/SuperTokens/headings.yml index 633b7f9e32..f7df77981a 100644 --- a/.vale/SuperTokens/headings.yml +++ b/.vale/SuperTokens/headings.yml @@ -13,7 +13,6 @@ exceptions: - GitHub - GitLab - LinkedIn - - SAML - Android - OAuth - SSO @@ -41,7 +40,6 @@ exceptions: - Proof Key for Code Exchange (PKCE) - Authorization Service Frontend - Authorization Service Backend - - JWT - Authorization Service - SuperTokens Session Access Tokens - M2M diff --git a/.vale/SuperTokens/links.yml b/.vale/SuperTokens/links.yml index 4537c94326..baf711a4a1 100644 --- a/.vale/SuperTokens/links.yml +++ b/.vale/SuperTokens/links.yml @@ -21,3 +21,7 @@ swap: # For the phrase 'this page' in Markdown and HTML links '\[this page\]\(.*?\)': "this page" '\s*this page\s*': "this page" + + # Other vague link labels + '\[this guide\]\(.*?\)': "this guide" + '\[this section\]\(.*?\)': "this section" diff --git a/.vale/SuperTokens/oxford-comma.yml b/.vale/SuperTokens/oxford-comma.yml index ad6d3300f4..464d6e6997 100644 --- a/.vale/SuperTokens/oxford-comma.yml +++ b/.vale/SuperTokens/oxford-comma.yml @@ -1,6 +1,6 @@ extends: existence message: "Use the Oxford comma in '%s'." scope: sentence -level: warning +level: suggestion tokens: - '(?:[^,]+,){1,}\s\w+\s(?:and|or)' diff --git a/.vale/SuperTokens/paragraph-start.yml b/.vale/SuperTokens/paragraph-start.yml index 0e2c32b933..8f76f45c89 100644 --- a/.vale/SuperTokens/paragraph-start.yml +++ b/.vale/SuperTokens/paragraph-start.yml @@ -1,8 +1,6 @@ extends: existence message: "Do not start a paragraph with a 'but'." -level: error +level: suggestion scope: paragraph -action: - name: remove tokens: - ^But diff --git a/.vale/SuperTokens/pronouns.yml b/.vale/SuperTokens/pronouns.yml index e08dfcf06d..0b4442ffbb 100644 --- a/.vale/SuperTokens/pronouns.yml +++ b/.vale/SuperTokens/pronouns.yml @@ -1,6 +1,6 @@ extends: existence message: "Avoid first-person pronouns such as '%s'." -level: warning +level: suggestion nonword: true tokens: - (?<=^|\s)I(?=\s) diff --git a/.vale/SuperTokens/sentence-start.yml b/.vale/SuperTokens/sentence-start.yml index 60a0f156d1..290cc1146b 100644 --- a/.vale/SuperTokens/sentence-start.yml +++ b/.vale/SuperTokens/sentence-start.yml @@ -1,6 +1,6 @@ extends: existence message: "Don't start a sentence with '%s'." -level: error +level: suggestion raw: - '(?:[;-]\s)so[\s,]|\bSo[\s,]' - '(?:[;-]\s)There\s(is|are)|\bThere\s(is|are)\b' diff --git a/.vale/SuperTokens/spelling.yml b/.vale/SuperTokens/spelling.yml index ad4e5f9ab9..9407f074c1 100644 --- a/.vale/SuperTokens/spelling.yml +++ b/.vale/SuperTokens/spelling.yml @@ -17,18 +17,16 @@ filters: - "refetched" - "liveness" - "embeddable" - - "recieve[s]" - "reachability" - "onboarding" - "refetching" - "refetch" - - "authorizer" - "Authorizer" - "upsert" - "Upsert" - "cron" - "Cron" - # Abbreviations and Brand Names + # Abbreviations - "SDK[s]" - "SDK's" - "OTP[s]" @@ -44,36 +42,18 @@ filters: - "DOM[s]" - "HTTPS" - "VPN[s]" - - "Cocoapods" - - "GitLab" - - "ngrok" - - "URL" - "npm" - "TOTP" - "SMS" - - "Okta" - - "Mailchimp" - - "Nginx" - "nginx" - "hostname" - - "Dashlane" - "Postgres" - - "PostgreSQL" - - "Netlify" - "php" - "golang" - - "Vue" - "JWK[s]" - - "Next.js" - - "Vercel" - - "Twilio" - - "MySQL" - "psql" - "bcrypt" - - "Supabase" - - "Hasura" - "OAuth" - - "GitHub" - "Validator" # Corect usage for these words are enforced in words.yml - "config" @@ -84,9 +64,6 @@ filters: # This usually happens when you highlight a line - "contactMethod" - "otherFrameworks" - # Prevents errors from Microsoft Entra expressions - # I don't know how to use vocabularies... yet - - "Entra" - "[Uu]nlink[ing]" - "deduplication" - "Callout" diff --git a/.vale/SuperTokens/tense.yml b/.vale/SuperTokens/tense.yml index 4f3e0eafa4..cbb6e9c226 100644 --- a/.vale/SuperTokens/tense.yml +++ b/.vale/SuperTokens/tense.yml @@ -1,10 +1,6 @@ extends: existence message: "Avoid temporal words like '%s'." ignorecase: true -level: warning +level: suggestion tokens: - currently - - now - - will - - won't - - "[a-zA-Z]*'ll" diff --git a/.vale/SuperTokens/weasel-words.yml b/.vale/SuperTokens/weasel-words.yml index 72d49c061c..5a3d10d557 100644 --- a/.vale/SuperTokens/weasel-words.yml +++ b/.vale/SuperTokens/weasel-words.yml @@ -1,7 +1,7 @@ extends: existence message: "Avoid filler words that don’t add value: '%s'" -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#4-conciseness -level: warning +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#4-conciseness +level: suggestion scope: text ignorecase: true diff --git a/.vale/SuperTokens/wordiness.yml b/.vale/SuperTokens/wordiness.yml index 6cfe9ab6f3..28728b1507 100644 --- a/.vale/SuperTokens/wordiness.yml +++ b/.vale/SuperTokens/wordiness.yml @@ -1,6 +1,6 @@ extends: substitution message: "Consider using '%s' instead of '%s'." -link: https://github.com/supertokens/docs/blob/master/v3/CONTRIBUTING.md#4-conciseness +link: https://github.com/supertokens/docs/blob/master/CONTRIBUTING.md#4-conciseness ignorecase: true level: suggestion action: diff --git a/.vale/SuperTokens/words.yml b/.vale/SuperTokens/words.yml index 22bba15f54..6c1af3b6cf 100644 --- a/.vale/SuperTokens/words.yml +++ b/.vale/SuperTokens/words.yml @@ -2,8 +2,7 @@ extends: substitution message: "Use '%s' instead of '%s'." ignorecase: false level: warning -action: - name: replace +scope: text swap: # bad: good config: configuration @@ -12,8 +11,8 @@ swap: multi factor: multi-factor Multifactor: Multi-factor Multi factor: Multi-factor - supertokens: "**SuperTokens**" - Supertokens: "**SuperTokens**" + supertokens: SuperTokens + Supertokens: SuperTokens (?:github|Github): GitHub (?:kubernetes|k8s|K8s|K8S): Kubernetes https: HTTPS diff --git a/.vale/SuperTokensErrors/typos.yml b/.vale/SuperTokensErrors/typos.yml index 05533a1f0d..05bfc7bb5a 100644 --- a/.vale/SuperTokensErrors/typos.yml +++ b/.vale/SuperTokensErrors/typos.yml @@ -10,5 +10,9 @@ swap: "it's APIs": "its APIs" experiece: experience awsome: awesome + recieve: receive + recieves: receives + recieved: received + recieving: receiving Preqrequisites: Prerequisites "Multi-Factory Authentication": "Multi-Factor Authentication" diff --git a/.vale/config/vocabularies/Authentication/accept.txt b/.vale/config/vocabularies/Authentication/accept.txt index 789508e5fd..aca3504d62 100644 --- a/.vale/config/vocabularies/Authentication/accept.txt +++ b/.vale/config/vocabularies/Authentication/accept.txt @@ -1 +1,65 @@ Microsoft Entra +Alamofire +Astro +Authlib +Axios +Blume +BoxyHQ +CAPTCHA +CocoaPods +Cutover +Dashlane +Dio +GitHub +GitLab +Gradle +Hasura +Hostname +IdP +Mailchimp +MySQL +Netlify +Next.js +Nginx +Okta +PostgreSQL +Rownd +Shiki +SuperTokens +Supabase +Twilio +Vercel +Vue +WebAuthn +Xcode +UIs +allowlist +allowlisted +authenticator's +captcha +colocated +compilable +cutover +else's +frontmatter +gzip +hardcoded +hardcodes +idempotency +lockfile +multitenant +namespace +namespaced +ngrok +overridable +plaintext +prebuild +pseudocode +rulebook +runtimes +subnet +typechecks +unpadded +untrusted +userinfo +walkthrough diff --git a/.vale/config/vocabularies/Authentication/reject.txt b/.vale/config/vocabularies/Authentication/reject.txt index e69de29bb2..ae4400d605 100644 --- a/.vale/config/vocabularies/Authentication/reject.txt +++ b/.vale/config/vocabularies/Authentication/reject.txt @@ -0,0 +1 @@ +Supertokens diff --git a/AGENTS.md b/AGENTS.md index 641aad5279..7df5374b5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,10 +2,11 @@ ## Build/Test Commands -- `npm run build` - Build the Docusaurus site +- `npm run build` - Build the Blume site - `npm run start` - Start development server -- `npm run typecheck` - Run TypeScript type checking +- `npm run typecheck` - Check Blume config, components, islands, and content - `npm run lint:prettier` - Format code with Prettier +- `npm run lint:prettier:check` - Check formatting without writing files - `npm run lint:vale` - Run Vale documentation linting - `npm test` - Run all tests with Vitest - `npm run test:watch` - Run tests in watch mode @@ -23,7 +24,8 @@ ## Project Structure -- Components in `src/components/` with feature-based organization -- Utilities in `src/lib/` and `src/hooks/` +- Astro components in `components/` +- React islands in `islands/` - Documentation content in `docs/` using MDX format +- Navigation metadata in colocated `meta.ts` files - Scripts for automation in `scripts/` directory diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 669e995120..2ff28159cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,10 +5,8 @@ This guide provides all the information needed to set up, build, and contribute ## Overview -The documentation project relies on the [Docusaurus](https://docusaurus.io/) framework to transform `MDX` files into an actual static website. MDX allows us to embed React components in the content, unlocking rich, interactive documentation experiences. - -That being said, there are several things that are added on top of the Docusaurus utilities in order to adjust the tooling to our needs. -Those are presented throughout this document. +The documentation site uses [Blume](https://useblume.dev/) to build Markdown and MDX into a static website. +Blume provides file-based navigation, local search, OpenAPI references, and built-in documentation components. ## How to run the project @@ -16,14 +14,14 @@ Those are presented throughout this document. To work with the documentation project locally, ensure you have the following tools installed: -- [Node.js](https://nodejs.org/en/download/) (version 18 or higher) +- [Node.js](https://nodejs.org/en/download/) 22.12 or newer ### Setup Steps -1. Install the dependencies: +1. Install dependencies: ```bash -npm install +npm ci ``` 2. Start the development server: @@ -32,39 +30,33 @@ npm install npm run start ``` +Vercel deploys previews and production from this repository. Production sets the public origin from Vercel's deployment environment; set `DOCS_PUBLIC_ORIGIN` only when a non-Vercel build needs canonical URLs and a sitemap. + ## Project Structure -The two main directories where you will work are: +The main directories are: -- `docs`: This is where the actual content sits. All the `.mdx` files are located here. -- `src`: This is where you will find the React components and the custom logic used in the website's functionality. +- `docs`: Documentation content and colocated `meta.ts` navigation files. +- `components`: Astro components registered for use in MDX. +- `islands`: Interactive React components. +- `openapi`: CDI and FDI OpenAPI specifications. +- `public`: Static assets. Below is a breakdown of the main directories and files in the project: ``` -├── docs # The actual documentation pages -│   ├── _templates # Templates that can be used as a starting point for new docs -│   ├── _blocks # Reusable MDX blocks -│   └── [section-name] -│   └── _category_.json # Info about how the folder will be shown in the left sidebar -├── src # The business logic of the website -│   ├── components -│   ├── context -│   ├── css -│   ├── hooks -│   ├── lib -│   ├── plugins # Plugins used by docusaurus during the build process -│   └── theme # Docusaurus components that get adjusted by us +├── docs # Documentation pages and meta.ts navigation +├── components # Astro MDX components +├── islands # Interactive React components +├── openapi # CDI and FDI specifications +├── public # Static assets ├── scripts -├── sidebars.ts -└── docusaurus.config.ts +└── blume.config.ts ``` ### Routing -The project uses file based routing so it's pretty straightforward to determine the actual path of a page. -Each subfolder has a `_category_.json` file that specifies the name of the sidebar category and the order of the pages inside that category. -Additionally, each MDX file has a `sidebar_position` property that specifies the order of the page inside the sidebar category. +The project uses file-based routing. Use a folder's `meta.ts` to configure category navigation and `sidebar.order` in page frontmatter to order pages. #### Where to place a new page @@ -158,6 +150,18 @@ But there are exceptions to most rules. - Avoid opinions, criticisms, or personal commentary. - Focus on presenting information that helps the user achieve their goals. +### Code group height + +Code blocks and `CodeGroup` examples are fully expanded by default. For an unusually long group, use the local +`maxHeight` extension sparingly: + +```mdx +{/* titled fences, Tab content, or dependent content */} +``` + +The value must be a non-negative number followed by `px`, `rem`, `em`, `vh`, `dvh`, `svh`, or `lvh`. Printed code +always expands to its full height. + ## How to test your changes ### Prerequisites @@ -183,7 +187,7 @@ npm install -g bun #### Docker -Used to run the code block validation and formatting. +Used to build the language-specific code block checkers. For install instructions check the [guide](https://docs.docker.com/get-docker/). ### Testing Steps @@ -192,14 +196,52 @@ For install instructions check the [guide](https://docs.docker.com/get-docker/). Use the following commands to perform linting checks on the entire project: -- `npm run lint:prettier`: Runs prettier on the `ts` files. +- `npm run lint:prettier`: Formats supported files with Prettier. +- `npm run lint:prettier:check`: Checks formatting without changing files. +- `npm run lint:code-blocks -- [path...]`: Checks registered languages, empty blocks, numeric highlight metadata, exclusion markers, and supported Prettier formatting without changing files. Paths can be files or directories; the command defaults to all files in `docs`. - `npm run lint:vale`: Runs Vale on Markdown files and high-signal typo rules on TSX files. +- `npm run validate`: Runs strict navigation and link validation. +- `npm run build`: Regenerates the route manifest and builds the site. #### Validating code blocks -Run `npm run check-code-blocks ` to validate all the code blocks for a particular language. +Use these commands for code blocks in Markdown and MDX files: + +- `npm run format-code-blocks [path...]`: Formats supported fenced TypeScript, JavaScript, JSON, YAML, and HTML. Paths can be files or directories; the command defaults to `docs`. +- `npm run write-code-blocks -- [path...]`: Extracts compilable code blocks from `.md` and `.mdx` files. Paths can be files or directories; the command defaults to all files in `docs`. It fails when a fence has a missing or unknown language. +- `npm run check-code-blocks `: Builds the Docker checker for the language. Validation runs as part of the image build; the image is not run afterward. + +#### Continuous integration + +For pull requests, code-block CI checks only changed documentation files and the languages generated from them. Changes to checker or test infrastructure trigger a full check, as do changes affecting more than 100 documentation files or a changed-file path list larger than 50 KB. Linting, infrastructure tests, and extraction run in parallel as their dependencies allow; language checks run after extraction. + +Set the repository variable `RUN_FULL_CODE_BLOCK_CHECKS=true` to run full code-block checks before the GitHub Build job in the non-pull-request deployment flow. If Vercel deploys directly from Git, configure Vercel to require the GitHub Build check externally. + +Run linting and extraction before a language checker so it does not validate stale snippets: + +```sh +npm run lint:code-blocks +npm run write-code-blocks +npm run check-code-blocks javascript +``` + +Checker names are `javascript`, `go`, `python`, `kotlin`, `swift`, `dart`, `php`, `java`, and `csharp`. TypeScript and JavaScript fences both use the `javascript` checker. + +Generated snippets are stored under `scripts/code-type-checking//snippets`, preserving the source path and including the fence's source line in each generated path. + +For an intentionally non-standalone snippet, add `check=false` and a non-empty, quoted reason to the code fence. This excludes it from formatting and type checking: + +````md +```ts check=false reason="Requires application context" +const app = getApplicationInstance(); +``` +```` + +An exclusion must contain exactly one unquoted `check=false` and exactly one closed, double-quoted, non-empty `reason`. Reason text may contain ASCII letters, numbers, spaces, and `. , ; : ! ? ( ) / _ + -`. Braces, quotes, backslashes, control characters, and newlines are not allowed because fence metadata is passed to Shiki. + +The lint and snippet writer reject malformed, duplicate, contradictory, or unsupported metadata. Invalid metadata does not skip formatting or extraction. Do not use this escape hatch for standalone snippets that can be made valid. -Code block validation is done in two steps: +Legacy exclusions using one of these exact comments as the first content line remain supported: -- First, we extract all the code blocks from the `MDX` files and save them in the `/scripts/code-type-checking//snippets` folder. -- Then, we load the code in a Docker image and run it to validate the code. This way we do not have to deal with installing different dependencies for different languages. +- `// exclude-from-type-checking` +- `# exclude-from-type-checking` diff --git a/README.md b/README.md index 7d8e2786b3..db0a7c8e90 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ This repository hosts the source files for the project's documentation. Our goal You can view the live documentation at [https://supertokens.com/docs](https://supertokens.com/docs). +## Microfrontend migration + +The docs-side migration implementation and remaining cutover blockers are tracked in +[`docs-microfrontend-migration-plan.md`](./docs-microfrontend-migration-plan.md). `blume.config.ts` owns the Vercel +microfrontends Vite plugin configuration. The `blume@1.5.3` patch only exposes configured plugins to generated Astro and +adds framework behavior needed for namespaced assets and endpoints; it does not own plugin selection. + +The local `microfrontends.jsonc` copy is contract-tested but the authoritative configuration remains in +`backend-api/apps/saas-dashboard-ui`. Shared-preview acceptance requires `MFE_PREVIEW_URL`; external SDK rewrite +credential forwarding is an explicit blocker before cutover, not a docs-code workaround. + ## Contributing **SuperTokens** provides an extensive authentication and authorization solution for various platforms and languages. diff --git a/blume.config.ts b/blume.config.ts new file mode 100644 index 0000000000..dba6284d20 --- /dev/null +++ b/blume.config.ts @@ -0,0 +1,109 @@ +import { defineConfig } from "blume"; +import { microfrontends } from "@vercel/microfrontends/experimental/vite"; + +import { ASK_AI_MODEL } from "./lib/ask-ai-config"; +import { configuredRedirects } from "./scripts/blume/configured-redirects.mjs"; +import { legacyRedirects } from "./scripts/migration/legacy-redirects"; + +const publicOrigin = process.env.DOCS_PUBLIC_ORIGIN ?? "https://supertokens.com"; +const posthogToken = process.env.PUBLIC_POSTHOG_PROJECT_TOKEN ?? process.env.POSTHOG_PROJECT_TOKEN; +const posthogHost = process.env.PUBLIC_POSTHOG_HOST ?? process.env.POSTHOG_HOST; +export default defineConfig({ + title: "SuperTokens Docs", + description: "Open Source User Authentication", + basePath: "/docs", + publicAssetBasePath: "/docs-assets", + logo: { + image: "/docs-assets/img/logos/supertokens-dark.svg", + text: "", + href: "https://supertokens.com", + }, + feedback: false, + theme: { + accent: { + light: "#a84f00", + dark: "#ffad4d", + }, + mode: "system", + fonts: { + body: "inter", + display: "inter", + mono: "ibm-plex-mono", + }, + }, + navigation: { + repo: false, + sidebar: { + display: "group", + }, + tabs: [ + { label: "Documentation", path: "/" }, + { label: "References", path: "/references" }, + ], + }, + markdown: { + code: { + wrap: true, + }, + codeBlocks: { + theme: { + light: "github-light", + dark: "one-dark-pro", + }, + }, + }, + search: { + provider: "orama", + }, + ai: { + llmsTxt: true, + ask: { + enabled: true, + provider: "gateway", + model: ASK_AI_MODEL, + suggestions: [ + { label: "How do I add SuperTokens to my app?", icon: "rocket" }, + { label: "Which authentication recipe should I use?", icon: "key-round" }, + { label: "How does session management work?", icon: "shield-check" }, + { label: "How do I self-host SuperTokens?", icon: "server" }, + ], + }, + mcp: { + enabled: true, + discovery: false, + route: "/docs/mcp", + name: "SuperTokens Documentation", + instructions: + "Search with specific authentication or deployment terms, then read the relevant overview and setup pages. Distinguish standalone passkey authentication under Authentication from passkeys used as an MFA factor under Additional Verification.", + }, + }, + openapi: { + enabled: true, + sources: [ + { label: "Core Driver Interface", route: "/references/cdi", spec: "./openapi/cdi.yml" }, + { label: "Frontend Driver Interface", route: "/references/fdi", spec: "./openapi/fdi.yml" }, + ], + }, + redirects: [...configuredRedirects, ...legacyRedirects], + deployment: { + output: "server", + adapter: "vercel", + site: publicOrigin, + }, + seo: { + agentReadability: true, + }, + vite: { + plugins: [microfrontends()], + }, + ...(posthogToken + ? { + analytics: { + posthog: { + key: posthogToken, + ...(posthogHost ? { host: posthogHost } : {}), + }, + }, + } + : {}), +}); diff --git a/components.json b/components.json new file mode 100644 index 0000000000..4326617bf1 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "theme.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "ui": "@/components/ui", + "lib": "@/lib", + "utils": "@/lib/utils", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components.ts b/components.ts new file mode 100644 index 0000000000..11ba2f78cb --- /dev/null +++ b/components.ts @@ -0,0 +1,47 @@ +import { defineComponents } from "blume"; + +import ApiRequestSnippet from "./components/ApiRequestSnippet.astro"; +import ApiReferenceDrawer from "./components/ApiReferenceDrawer.astro"; +import AnchoredHeading from "./components/AnchoredHeading.astro"; +import ConditionalContent from "./components/ConditionalContent.astro"; +import CodeGroup from "./components/CodeGroup.astro"; +import ContentOption from "./components/ContentOption.astro"; +import ContextCondition from "./components/ContextCondition.astro"; +import DependentContent from "./components/DependentContent.astro"; +import DynamicContent from "./components/DynamicContent.astro"; +import Header from "./components/Header.astro"; +import NextjsRouterTypeSelect from "./components/NextjsRouterTypeSelect.astro"; +import PaidFeatureCallout from "./components/PaidFeatureCallout.astro"; +import PasswordlessRecipeForm from "./components/PasswordlessRecipeForm.astro"; +import Tab from "./components/Tab.astro"; +import TableOfContents from "./components/TableOfContents.astro"; +import Tabs from "./components/Tabs.astro"; +import TenantTypeSwitch from "./components/TenantTypeSwitch.astro"; +import UITypeSwitch from "./components/UITypeSwitch.astro"; +import VariantContent from "./components/VariantContent.astro"; + +export default defineComponents({ + layout: { + Footer: ApiReferenceDrawer, + Header, + TableOfContents, + }, + mdx: { + AnchoredHeading, + ApiRequestSnippet, + CodeGroup, + ConditionalContent, + ContentOption, + ContextCondition, + DependentContent, + DynamicContent, + NextjsRouterTypeSelect, + PaidFeatureCallout, + PasswordlessRecipeForm, + Tab, + Tabs, + TenantTypeSwitch, + UITypeSwitch, + VariantContent, + }, +}); diff --git a/components/AnchoredHeading.astro b/components/AnchoredHeading.astro new file mode 100644 index 0000000000..bdb2075087 --- /dev/null +++ b/components/AnchoredHeading.astro @@ -0,0 +1,15 @@ +--- +interface Props { + id: string; + level: 2 | 3 | 4 | 5 | 6; +} + +const { id, level } = Astro.props; +const Heading = `h${level}` as const; +--- + + + + + + diff --git a/components/ApiReferenceDrawer.astro b/components/ApiReferenceDrawer.astro new file mode 100644 index 0000000000..c85a3c51bc --- /dev/null +++ b/components/ApiReferenceDrawer.astro @@ -0,0 +1,365 @@ + +
+
+
+

API reference

+

+ API schema and response details +

+
+ + + +
+
+
+
+ + diff --git a/components/ApiRequestSnippet.astro b/components/ApiRequestSnippet.astro new file mode 100644 index 0000000000..c590a3fea9 --- /dev/null +++ b/components/ApiRequestSnippet.astro @@ -0,0 +1,174 @@ +--- +import specs from "blume:openapi"; +import CodeBlock from "blume/components/content/CodeBlock.astro"; +import { + mergeParameters, + type ParameterLike, + resolveComponentRef, + type SchemaLike, +} from "blume/components/openapi/helpers.ts"; +import { operationModel } from "blume/components/openapi/operation-model.ts"; +import { buildRequest, defaultValues, paramKey } from "blume/components/openapi/request.ts"; +import { + effectiveSecurity, + resolveSecurity, + type SecurityRequirementLike, + type SecuritySchemeLike, +} from "blume/components/openapi/security.ts"; +import { withBase } from "blume/components/islands/base-path.ts"; + +import { buildApiRequestSnippetLanguages } from "../lib/api-request-snippets"; +import SelectionGroup from "./SelectionGroup.astro"; +import Tab from "./Tab.astro"; + +interface Props { + body?: JsonObject; + curlConfig?: string; + operationId: string; + path?: Record; + query?: Record; + source: "cdi" | "fdi"; +} + +type JsonValue = boolean | null | number | string | JsonValue[] | JsonObject; + +interface JsonObject { + [key: string]: JsonValue; +} + +interface MediaTypeLike { + schema?: SchemaLike; + example?: unknown; + examples?: Record; +} + +interface RequestBodyLike { + $ref?: string; + content?: Record; +} + +interface OperationLike { + parameters?: ParameterLike[]; + requestBody?: RequestBodyLike; + security?: SecurityRequirementLike[]; +} + +const { body, curlConfig, operationId, path = {}, query = {}, source } = Astro.props; +const sourceTokens = { + cdi: "references-cdi", + fdi: "references-fdi", +} as const; +const sourceToken = sourceTokens[source]; +if (!sourceToken) { + throw new Error(`[ApiRequestSnippet] Invalid OpenAPI source "${source}" for operation "${operationId}".`); +} +const spec = specs[sourceToken]; +if (!spec) { + throw new Error(`[ApiRequestSnippet] OpenAPI source "${sourceToken}" was not found.`); +} +const ref = Object.values(spec?.operations ?? {}).find((candidate) => candidate.operationId === operationId); +if (!ref) { + throw new Error(`[ApiRequestSnippet] Operation "${operationId}" was not found in "${sourceToken}".`); +} +const doc = (spec?.document ?? {}) as { + paths?: Record & { parameters?: ParameterLike[] }>; + components?: { + schemas?: Record; + parameters?: Record; + requestBodies?: Record; + securitySchemes?: Record; + }; + security?: SecurityRequirementLike[]; +}; +const pathItem = ref ? doc.paths?.[ref.path] : undefined; +const operation = (pathItem && ref ? pathItem[ref.method] : undefined) as OperationLike | undefined; +if (!operation) { + throw new Error( + `[ApiRequestSnippet] Operation "${operationId}" points to missing ${ref.method.toUpperCase()} ${ref.path}.`, + ); +} +const components = doc.components ?? {}; +const parameters = mergeParameters(pathItem?.parameters, operation?.parameters, components); +const resolvedRequestBody = operation.requestBody + ? resolveComponentRef(operation.requestBody, components, "requestBodies") + : undefined; +const requestBody = resolvedRequestBody + ? { + ...resolvedRequestBody, + content: Object.fromEntries( + Object.entries(resolvedRequestBody.content ?? {}).map(([contentType, media]) => [ + contentType, + { + ...media, + example: media.example ?? media.examples?.default?.value, + }, + ]), + ), + } + : undefined; +const security = resolveSecurity( + effectiveSecurity(operation?.security, doc.security), + doc.components?.securitySchemes, +); +const model = operationModel({ + method: ref.method, + parameters, + path: ref.path, + requestBody, + schemas: doc.components?.schemas ?? {}, + security, + servers: [{ url: source === "cdi" ? "" : "" }], +}); +const values = defaultValues(model); +if (body !== undefined) { + values.body = JSON.stringify(body, null, 2); +} +for (const [name, value] of Object.entries(query)) { + const parameter = model.params.find((candidate) => candidate.in === "query" && candidate.name === name); + if (!parameter) { + throw new Error( + `[ApiRequestSnippet] Query override "${name}" is not declared by operation "${operationId}".`, + ); + } + values.params[paramKey(parameter)] = value; +} +const sample = buildRequest(model, values); +for (const [name, value] of Object.entries(path)) { + const placeholder = `{${name}}`; + if (!sample.url.includes(placeholder)) { + throw new Error(`[ApiRequestSnippet] Path override "${name}" is not present in operation "${operationId}".`); + } + sample.url = sample.url.replaceAll(placeholder, encodeURIComponent(value)); +} + +const languages = buildApiRequestSnippetLanguages({ curlConfig }); +const namespace = `api-request-${crypto.randomUUID()}`; +const embedRoute = withBase(`/docs/api-reference-embed/${source}/${ref.key}`); +const specRoute = withBase(`/docs/api-spec/${source}.json`); +--- + +
+ + + API Reference + + { + languages.map((language) => ( + + + + )) + } + +
diff --git a/components/CodeGroup.astro b/components/CodeGroup.astro new file mode 100644 index 0000000000..4f7baf4cbc --- /dev/null +++ b/components/CodeGroup.astro @@ -0,0 +1,20 @@ +--- +import type { CodeGroupProps } from "blume/components"; + +import SelectionGroup from "./SelectionGroup.astro"; +import type { CodeGroupMaxHeight } from "./code-group"; +import type { TabGroup } from "./tab-groups"; + +interface Props extends CodeGroupProps { + group?: TabGroup; + maxHeight?: CodeGroupMaxHeight; + passive?: boolean; + secondaryControls?: string; +} + +const props = Astro.props as Props; +--- + + + + diff --git a/components/ConditionalContent.astro b/components/ConditionalContent.astro new file mode 100644 index 0000000000..6b758c40a1 --- /dev/null +++ b/components/ConditionalContent.astro @@ -0,0 +1,15 @@ +--- +import VariantContent from "./VariantContent.astro"; + +interface Props { + propertyName: string; + condition: string; +} + +const { propertyName, condition } = Astro.props; +const storageKey = propertyName === "nextjsRouterType" ? "nextjs-router-type" : propertyName; +--- + + + + diff --git a/components/ContentOption.astro b/components/ContentOption.astro new file mode 100644 index 0000000000..18c81b2586 --- /dev/null +++ b/components/ContentOption.astro @@ -0,0 +1,21 @@ +--- +interface Props { + title: string; + value: string; +} + +const { title, value } = Astro.props; +const sectionId = `docs-content-option-${crypto.randomUUID()}`; +--- + +
+ + +
diff --git a/components/ContextCondition.astro b/components/ContextCondition.astro new file mode 100644 index 0000000000..f254d1e3e7 --- /dev/null +++ b/components/ContextCondition.astro @@ -0,0 +1,19 @@ +--- +import VariantContent from "./VariantContent.astro"; + +interface Props { + condition: Record; +} + +const { condition } = Astro.props; +const [propertyName, value] = Object.entries(condition)[0] || []; +const storageKeys: Record = { + "recipes.passwordless.contactMethod": "passwordless-contact-method", + "recipes.passwordless.flowType": "passwordless-flow-type", +}; +const storageKey = storageKeys[propertyName] || propertyName; +--- + + + + diff --git a/components/DependentContent.astro b/components/DependentContent.astro new file mode 100644 index 0000000000..9d1728dcd9 --- /dev/null +++ b/components/DependentContent.astro @@ -0,0 +1,44 @@ +--- +import DependentContentController from "../islands/DependentContentController"; +import { tabGroupDefault, tabGroupLabel, tabGroupNames } from "./tab-groups"; + +interface Props { + defaultValue?: string; + group: string; + label?: string; + passive?: boolean; +} + +const { defaultValue, group, label, passive = false } = Astro.props; +const knownGroup = tabGroupNames.find((candidate) => candidate === group); +const wrapperId = `docs-dependent-content-${crypto.randomUUID()}`; +const fallbackHostId = `${wrapperId}-accessory`; +--- + +
+ { + !passive && ( +
+ ) + } + + +
diff --git a/components/DocsPreferences.astro b/components/DocsPreferences.astro new file mode 100644 index 0000000000..c34de089a7 --- /dev/null +++ b/components/DocsPreferences.astro @@ -0,0 +1,5 @@ +--- +import DocsPreferences from "../islands/DocsPreferences"; +--- + + diff --git a/components/DynamicContent.astro b/components/DynamicContent.astro new file mode 100644 index 0000000000..5d9c2724d6 --- /dev/null +++ b/components/DynamicContent.astro @@ -0,0 +1,97 @@ +--- +import { frameworkTabGroup, languageFrameworkGroup, tabGroupDefault, tabGroups } from "./tab-groups"; + +interface Props { + framework?: string; + language?: string; +} + +const { framework, language } = Astro.props; +const frameworkGroup = framework + ? language + ? languageFrameworkGroup(language) + : frameworkTabGroup(framework) + : undefined; +const languageValues = new Set(Object.values(tabGroups["backend-language"].options)); +const frameworkValues = new Set(frameworkGroup ? Object.values(tabGroups[frameworkGroup].options) : []); +if (language && !languageValues.has(language)) { + throw new Error(`DynamicContent received unknown backend language "${language}".`); +} +if (framework && (!frameworkGroup || !frameworkValues.has(framework))) { + throw new Error( + `DynamicContent received framework "${framework}"${language ? ` for unsupported language "${language}"` : ""}.`, + ); +} +const matchesDefaultLanguage = !language || tabGroupDefault("backend-language") === language; +const matchesDefaultFramework = + !framework || (frameworkGroup ? tabGroupDefault(frameworkGroup) === framework : false); +--- + + + + diff --git a/components/Header.astro b/components/Header.astro new file mode 100644 index 0000000000..5e1db960a8 --- /dev/null +++ b/components/Header.astro @@ -0,0 +1,163 @@ +--- +import data from "blume:data"; +import { withBase } from "blume/components/islands/base-path.ts"; +import { EN_UI } from "blume/core/i18n-ui.ts"; +import type { LocaleSwitchOption, Navigation } from "blume/core/types.ts"; +import Icon from "blume/components/Icon.astro"; +import LanguageSwitcher from "blume/components/layout/LanguageSwitcher.astro"; +import Logo from "blume/components/layout/Logo.astro"; +import { activeTabForRoute } from "blume/components/layout/nav-utils.ts"; +import NavSelector from "blume/components/layout/NavSelector.astro"; +import Ask from "./ask-ai/AskAI.astro"; +import Search from "./Search.astro"; + +interface Props { + askEnabled?: boolean; + hasDrawer?: boolean; + hasSidebar?: boolean; + localeSwitch?: LocaleSwitchOption[]; + logo?: { alt: string; href: string; text?: string } | null; + navStrings?: Record; + navigation: Navigation; + route: string; + searchEnabled: boolean; + searchLocale?: string; + searchStrings?: Record; + site: { title: string }; + switcherStrings?: Record; +} + +const { + askEnabled = Boolean(data.config.ask), + hasDrawer = true, + hasSidebar = true, + localeSwitch, + logo, + navStrings, + navigation, + route, + searchEnabled, + searchLocale, + searchStrings, + site, + switcherStrings, +} = Astro.props as Props; +const n = { ...EN_UI.nav, ...navStrings }; +const activeTab = activeTabForRoute(navigation.tabs, route); +const showNavToggle = hasDrawer && (hasSidebar || navigation.tabs.length > 0); +const tabsNavClass = hasSidebar + ? "st-header-tabs hidden h-full items-center gap-1 md:flex" + : "st-header-tabs hidden h-full items-center gap-1 lg:flex"; +const iconButton = + "inline-flex size-11 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground sm:size-9"; +const accessibleLogo = logo ? { ...logo, text: logo.text || site.title } : logo; +const clickScript = `(()=>{const d=document.documentElement;const dr=()=>{const h=document.querySelector("[data-blume-header]");if(h){d.style.setProperty("--blume-drawer-top",h.getBoundingClientRect().bottom+"px");}};const sn=()=>{const drawer=document.querySelector("[data-blume-nav-drawer]");if(drawer&&!drawer.id){drawer.id="blume-nav-drawer";}const open=d.hasAttribute("data-blume-nav-open");document.querySelectorAll("[data-blume-header] [data-blume-nav-toggle]").forEach((button)=>{button.setAttribute("aria-expanded",String(open));button.setAttribute("aria-controls",drawer?.id||"blume-nav-drawer");});};const st=()=>{document.querySelectorAll("[data-blume-theme-toggle]").forEach((button)=>{const dark=d.dataset.theme==="dark";const label=dark?button.dataset.labelLight:button.dataset.labelDark;if(label){button.setAttribute("aria-label",label);button.setAttribute("title",label);}});};const si=()=>{sn();st();};document.addEventListener("click",(e)=>{const t=e.target.closest("[data-blume-theme-toggle]");if(t){const next=d.dataset.theme==="dark"?"light":"dark";const s=document.createElement("style");s.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important}"));document.head.appendChild(s);d.dataset.theme=next;try{localStorage.setItem("blume-theme",next);}finally{window.getComputedStyle(d).opacity;setTimeout(()=>s.remove(),1);}st();}const n=e.target.closest("[data-blume-nav-toggle]");if(n){const open=d.toggleAttribute("data-blume-nav-open");if(open){dr();}d.style.overflow=open?"hidden":"";sn();}});addEventListener("resize",()=>{if(!d.hasAttribute("data-blume-nav-open")){return;}if(matchMedia("(min-width:64rem)").matches){d.removeAttribute("data-blume-nav-open");d.style.overflow="";sn();}else{dr();}});document.addEventListener("DOMContentLoaded",si,{once:true});document.addEventListener("astro:page-load",si);si();})();`; +--- + +
+ { + showNavToggle && ( + + ) + } +
+
+ +
+ { + navigation.selectors.length > 0 && ( + + ) + } + { + navigation.tabs.length > 0 && ( + + ) + } +
+
+ { + localeSwitch && localeSwitch.length > 1 && ( + + ) + } + { + searchEnabled && ( + + ) + } +
+ + Dashboard + + + + + + + {askEnabled && } +
+
+ + diff --git a/components/NextjsRouterTypeSelect.astro b/components/NextjsRouterTypeSelect.astro new file mode 100644 index 0000000000..59082ba146 --- /dev/null +++ b/components/NextjsRouterTypeSelect.astro @@ -0,0 +1,10 @@ +--- +import VariantSwitch from "./VariantSwitch.astro"; +--- + + diff --git a/components/PaidFeatureCallout.astro b/components/PaidFeatureCallout.astro new file mode 100644 index 0000000000..c215b59966 --- /dev/null +++ b/components/PaidFeatureCallout.astro @@ -0,0 +1,155 @@ +--- +import { withBase } from "blume/components/islands/base-path.ts"; + +interface Props { + managedOnly?: boolean; +} + +const { managedOnly = false } = Astro.props; +const licenseKeyGuide = withBase("/deployment/self-host-supertokens#5-add-license-keys"); +--- + + + + + +
+
+

Enable paid features

+ +
+ +
+

This feature is only available to paid users. Follow the instructions below to enable it.

+ + + + { + managedOnly && ( +

+ This feature is available only with the SuperTokens Managed Service. +

+ ) + } + +
+

Managed Service

+
    +
  1. + Sign in to the SuperTokens dashboard. +
  2. +
  3. + Select the managed service option from the service type select component. +
  4. +
  5. + Select your core instance from the next elemenet or create a new one. +
  6. +
  7. + Open Features sub-page and enable the required ones. +
  8. +
+
+ + { + !managedOnly && ( +
+

Self Hosted

+
    +
  1. + Sign in to the SuperTokens dashboard. +
  2. +
  3. + Select the self-hosted option from the service type select component. +
  4. +
  5. + Select your license key from the next elemenet or create a new one. Then enable the required features. +
  6. +
  7. + + If the key is not yet configured, add it to your Core service. If your + Core already uses this key, no configuration changes are required. + +
  8. +
+
+ ) + } +
+ +
+ +
+
+
+
+ + diff --git a/components/PasswordlessRecipeForm.astro b/components/PasswordlessRecipeForm.astro new file mode 100644 index 0000000000..314a7fea12 --- /dev/null +++ b/components/PasswordlessRecipeForm.astro @@ -0,0 +1,24 @@ +--- +import VariantSwitch from "./VariantSwitch.astro"; +--- + +
+ +
+ + diff --git a/components/Search.astro b/components/Search.astro new file mode 100644 index 0000000000..6050676575 --- /dev/null +++ b/components/Search.astro @@ -0,0 +1,356 @@ +--- +import BlumeSearch from "blume/components/layout/Search.astro"; + +const props = Astro.props; +--- + + + + diff --git a/components/SelectionGroup.astro b/components/SelectionGroup.astro new file mode 100644 index 0000000000..9d241f587a --- /dev/null +++ b/components/SelectionGroup.astro @@ -0,0 +1,454 @@ +--- +import type { CodeGroupProps } from "blume/components"; +import BlumeCodeGroup from "blume/components/content/CodeGroup.astro"; + +import PrimaryTabController from "../islands/PrimaryTabController"; +import CodeOptionController from "../islands/CodeOptionController"; +import { validateCodeGroupMaxHeight, type CodeGroupMaxHeight } from "./code-group"; +import { tabGroupControl, tabGroupDefault, tabGroupLabel, tabGroups, type TabGroup } from "./tab-groups"; + +interface Props extends CodeGroupProps { + control?: "select" | "tabs"; + group?: TabGroup; + label?: string; + maxHeight?: CodeGroupMaxHeight; + passive?: boolean; + secondaryControls?: string; + showOptionMarks?: boolean; +} + +const { + control: requestedControl, + group, + label, + maxHeight, + passive = false, + secondaryControls, + showOptionMarks = true, + ...props +} = Astro.props as Props; +const validatedMaxHeight = validateCodeGroupMaxHeight(maxHeight); +const control = requestedControl ?? (group ? tabGroupControl(group) : "tabs"); +const wrapperId = `docs-selection-group-${crypto.randomUUID()}`; +const accessoryHostId = `${wrapperId}-accessory`; +const hasPrimarySelect = control === "select"; +const componentProps = { + dropdown: group || hasPrimarySelect ? false : props.dropdown, + tabValues: group ? JSON.stringify(tabGroups[group].options) : undefined, +}; +--- + + + + + + diff --git a/components/Tab.astro b/components/Tab.astro new file mode 100644 index 0000000000..52bbaf1412 --- /dev/null +++ b/components/Tab.astro @@ -0,0 +1,47 @@ +--- +import Icon from "blume/components/Icon.astro"; + +interface Props { + class?: string; + icon?: string; + id?: string; + style?: string; + title: string; + value?: string; +} + +const { class: className, icon, id, style, title, value } = Astro.props; +const slugify = (input: string): string => + input + .toLowerCase() + .trim() + .replaceAll(/[^\w\s-]/gu, "") + .replaceAll(/[\s_]+/gu, "-") + .replaceAll(/-+/gu, "-") + .replaceAll(/^-|-$/gu, ""); +const tabValue = value ?? id ?? slugify(title); +--- + +
pre]:-mx-4 [&>pre]:-my-3 [&>:first-child]:mt-0! [&>:last-child]:mb-0!", + className, + ]} + data-blume-tab-panel + data-docs-code-option + style={style} + data-icon={icon} + data-tab-id={tabValue} + data-title={title} + id={id} + role="tabpanel" +> + + { + icon && ( + + ) + } +
diff --git a/components/TableOfContents.astro b/components/TableOfContents.astro new file mode 100644 index 0000000000..6004ecbfc6 --- /dev/null +++ b/components/TableOfContents.astro @@ -0,0 +1,206 @@ +--- +import type { ComponentProps } from "astro/types"; +import type { Heading } from "blume/core/types.ts"; + +import DocsPreferences from "./DocsPreferences.astro"; + +interface Props extends ComponentProps<"div"> { + headings: Heading[]; + title: string; + variant: "desktop" | "mobile"; +} + +const props = Astro.props as Props; +--- + +{props.variant === "desktop" && } +{ + props.headings.length > 0 && props.variant === "mobile" && ( +
+ + {props.title} + + + +
+ ) +} +{ + props.headings.length > 0 && props.variant === "desktop" && ( + + ) +} + + diff --git a/components/Tabs.astro b/components/Tabs.astro new file mode 100644 index 0000000000..152777c4b4 --- /dev/null +++ b/components/Tabs.astro @@ -0,0 +1,10 @@ +--- +import type { TabsProps } from "blume/components"; +import BlumeTabs from "blume/components/content/Tabs.astro"; + +const { hash = false, sync = false, ...props } = Astro.props as TabsProps; +--- + + + + diff --git a/components/TenantTypeSwitch.astro b/components/TenantTypeSwitch.astro new file mode 100644 index 0000000000..bda96145a3 --- /dev/null +++ b/components/TenantTypeSwitch.astro @@ -0,0 +1,10 @@ +--- +import VariantSwitch from "./VariantSwitch.astro"; +--- + + diff --git a/components/UITypeSwitch.astro b/components/UITypeSwitch.astro new file mode 100644 index 0000000000..abb16c8a81 --- /dev/null +++ b/components/UITypeSwitch.astro @@ -0,0 +1,23 @@ +--- +import VariantSwitch from "./VariantSwitch.astro"; +--- + + diff --git a/components/VariantContent.astro b/components/VariantContent.astro new file mode 100644 index 0000000000..5ce9dec9c6 --- /dev/null +++ b/components/VariantContent.astro @@ -0,0 +1,83 @@ +--- +interface Props { + storageKey: string; + value: string; +} + +const { storageKey, value } = Astro.props; +const defaults: Record = { + "ui-type": "prebuilt", + "tenant-type": "single", + "nextjs-router-type": "app-router", + "passwordless-contact-method": "EMAIL", + "passwordless-flow-type": "MAGIC_LINK", +}; +--- + + + + diff --git a/components/VariantSwitch.astro b/components/VariantSwitch.astro new file mode 100644 index 0000000000..c9f6c40f17 --- /dev/null +++ b/components/VariantSwitch.astro @@ -0,0 +1,210 @@ +--- +import { Code2Icon, LayoutTemplateIcon } from "lucide-react"; + +interface Option { + description?: string; + icon?: "code-2" | "layout-template"; + label: string; + value: string; +} + +interface Props { + storageKey: string; + options: Option[]; + defaultValue: string; + label: string; +} + +const { storageKey, options, defaultValue, label } = Astro.props; +const switchId = crypto.randomUUID(); +const optionIcons = { + "code-2": Code2Icon, + "layout-template": LayoutTemplateIcon, +}; +--- + +
+ {label} +
+ { + options.map((option) => { + const Icon = option.icon ? optionIcons[option.icon] : undefined; + return ( + + ); + }) + } +
+
+ + + + diff --git a/components/ask-ai/AskAI.astro b/components/ask-ai/AskAI.astro new file mode 100644 index 0000000000..16c2c8a48d --- /dev/null +++ b/components/ask-ai/AskAI.astro @@ -0,0 +1,100 @@ +--- +import data from "blume:data"; +import type { UIStrings } from "blume/core/i18n-ui.ts"; +import { resolveIcon } from "blume/theme/icons.ts"; + +import AskAI from "./ask-ai"; + +interface Props { + strings?: UIStrings["ask"]; +} + +const { strings } = Astro.props; + +const svgFor = (name: string | undefined): string | null => { + const resolved = resolveIcon(name ?? "sparkles") ?? resolveIcon("sparkles"); + return resolved + ? `` + : null; +}; + +const bodyFor = (name: string): string => resolveIcon(name)?.body ?? ""; +const icons = { + arrowUp: bodyFor("arrow-up"), + chat: bodyFor("message-circle"), + clear: bodyFor("trash-2"), + close: bodyFor("chevrons-right"), + copy: bodyFor("copy"), + thumbsDown: bodyFor("thumbs-down"), + thumbsUp: bodyFor("thumbs-up"), +}; +const suggestions = (data.config.ask?.suggestions ?? []).map((suggestion) => ({ + icon: svgFor(suggestion.icon), + label: suggestion.label, +})); +const posthogEnabled = Boolean(process.env.PUBLIC_POSTHOG_PROJECT_TOKEN ?? process.env.POSTHOG_PROJECT_TOKEN); +const posthogToken = process.env.PUBLIC_POSTHOG_PROJECT_TOKEN ?? process.env.POSTHOG_PROJECT_TOKEN; +const posthogHost = process.env.PUBLIC_POSTHOG_HOST ?? process.env.POSTHOG_HOST ?? "https://us.i.posthog.com"; +const devPosthogSnippet = + import.meta.env.DEV && posthogToken + ? `!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(t){return u.toString(1)},o="capture register register_once getFeatureFlag isFeatureEnabled getSurveys getActiveMatchingSurveys".split(" "),n=0;n + +{devPosthogSnippet && -``` - -Then call the `supertokensEmailVerification.init` function as shown below - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensEmailVerification from "supertokens-web-js-script/recipe/emailverification"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - // highlight-start - supertokensEmailVerification.init(), - supertokensSession.init(), - ], -}); -``` - - - - - - - - -:::success -No specific action required here. -::: - - - + + ### 3. Send the email verification email After a user signs up, or when the email verification validators fail, you need to tell the user about the email verification process. Redirect them to a screen that informs them about the current status and call the verification API. - - - - - + + +Create a new screen on your app that asks the user to enter their email to receive an email. This screen should ideally link to the sign in form. +Once the user has entered their email, you can call the following API to send an email verification email to that user: + + + + + + + ```tsx import { sendVerificationEmail } from "supertokens-web-js/recipe/emailverification"; async function sendEmail() { - try { - let response = await sendVerificationEmail(); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - // This can happen if the info about email verification in the session was outdated. - // Redirect the user to the home page - window.location.assign("/home"); - } else { - // email was sent successfully. - window.alert("Please check your email and click the link in it") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + try { + let response = await sendVerificationEmail(); + if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { + // This can happen if the info about email verification in the session was outdated. + // Redirect the user to the home page + window.location.assign("/home"); + } else { + // email was sent successfully. + window.alert("Please check your email and click the link in it"); } -} -``` - - - - -```tsx -import supertokensEmailVerification from "supertokens-web-js/recipe/emailverification"; - -async function sendEmail() { - try { - let response = await supertokensEmailVerification.sendVerificationEmail(); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - // This can happen if the info about email verification in the session was outdated. - // Redirect the user to the home page - window.location.assign("/home"); - } else { - // email was sent successfully. - window.alert("Please check your email and click the link in it") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); } + } } ``` - - - - - - - - - - -Create a new screen on your app that asks the user to enter their email to receive an email. This screen should ideally link to the sign in form. -Once the user has enters their email, you can call the following API to send an email verification email to that user: - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify/token' \ +curl --location --request POST '/auth/user/email/verify/token' \ --header 'Authorization: Bearer ...' ``` + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: An email was successfully sent to the user. - `status: "EMAIL_ALREADY_VERIFIED_ERROR"`: This status can return if the info about email verification in the session was outdated. Redirect the user to the home page. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -:::info Multi Tenancy +:::info[Multi Tenancy] You do not need to add the tenant ID to the path here because the backend fetches the `tenantId` of the user from the session token. ::: + + - - - -:::note -The API for sending an email verification email requires an active session. If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. +:::note[The API for sending an email verification email requires an active session. If you are using the frontend SDKs, then the session tokens should automatically get attached to the request.] ::: #### Change the email verification link @@ -460,52 +366,47 @@ By default, the email verification link points to the `websiteDomain` configured That would be the `/auth/verify-email` route if `/auth` is the value of `websiteBasePath`. If you want to change this to something different, follow the next example: - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailVerification.init({ - mode: "OPTIONAL", - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail(input) { - return originalImplementation.sendEmail({ - ...input, - emailVerifyLink: input.emailVerifyLink.replace( - // This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/verify-email` - "http://localhost:3000/auth/verify-email", - "http://localhost:3000/your/path" - ) - } - ) - }, - } - } - } - // highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail(input) { + return originalImplementation.sendEmail({ + ...input, + emailVerifyLink: input.emailVerifyLink.replace( + // This is: `/auth/verify-email` + "http://localhost:3000/auth/verify-email", + "http://localhost:3000/your/path", + ), + }); + }, + }; + }, + }, + }), + ], }); ``` - - - + + ```go import ( "strings" @@ -521,13 +422,12 @@ func main() { RecipeList: []supertokens.Recipe{ emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeOptional, - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { - // This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/verify-email` + // This is: `/auth/verify-email` input.EmailVerification.EmailVerifyLink = strings.Replace( input.EmailVerification.EmailVerifyLink, "http://localhost:3000/auth/verify-email", @@ -538,16 +438,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailverification from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig @@ -558,15 +456,13 @@ from typing import Dict, Any def custom_email_delivery(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email - # highlight-start async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: - # This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/verify-email` + # This is: `/auth/verify-email` template_vars.email_verify_link = template_vars.email_verify_link.replace( "http://localhost:3000/auth/verify-email", "http://localhost:3000/your/path") return await original_send_email(template_vars, user_context) - # highlight-end original_implementation.send_email = send_email return original_implementation @@ -575,20 +471,18 @@ def custom_email_delivery(original_implementation: EmailDeliveryOverrideInput) - init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailverification.init( mode="OPTIONAL", - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_email_delivery)) ] ) ``` + + - - - -:::info Multi Tenancy +:::info[Multi Tenancy] For a multi tenant setup, the input to the `sendEmail` function also contains the `tenantId`. You can use this to determine the correct value to set for the `websiteDomain` in the generated link. @@ -599,110 +493,85 @@ For a multi tenant setup, the input to the `sendEmail` function also contains th Once the user clicks the email verification link, and it opens your app, call the following function. It extracts the token and `tenantId` (if you use a multi tenant setup) from the link and calls the token verification API. - - - - + + +When the user clicks the email verification link, and it opens as a deep link into your mobile app, you can remove the token and call the verification API. + + + + + + ```tsx import { verifyEmail } from "supertokens-web-js/recipe/emailverification"; async function consumeVerificationCode() { - try { - let response = await verifyEmail(); - if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { - // This can happen if the verification code is expired or invalid. - // You should ask the user to retry - window.alert("Oops! Seems like the verification link expired. Please try again") - window.location.assign("/auth/verify-email") // back to the email sending screen. - } else { - // email was verified successfully. - window.location.assign("/home") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + try { + let response = await verifyEmail(); + if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { + // This can happen if the verification code is expired or invalid. + // You should ask the user to retry + window.alert("Oops! Seems like the verification link expired. Please try again"); + window.location.assign("/auth/verify-email"); // back to the email sending screen. + } else { + // email was verified successfully. + window.location.assign("/home"); } -} -``` - - - - -```tsx -import supertokensEmailVerification from "supertokens-web-js/recipe/emailverification"; - -async function consumeVerificationCode() { - try { - let response = await supertokensEmailVerification.verifyEmail(); - if (response.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { - // This can happen if the verification code is expired or invalid. - // You should ask the user to retry - window.alert("Oops! Seems like the verification link expired. Please try again") - window.location.assign("/auth/verify-email") // back to the email sending screen. - } else { - // email was verified successfully. - window.location.assign("/home") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); } + } } ``` - - - - - - - - - - -When the user clicks the email verification link, and it opens as a deep link into your mobile app, you can remove the token and call the verification API. - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "method": "token", - "token": "ZTRiOTBjNz...jI5MTZlODkxw" -}' -``` - -:::info Multi Tenancy - -For a multi tenancy setup, the `` value can fetch from `tenantId` query parameter from the email verification link. If it's not there in the link, you can use the value `"public"` (which is the default tenant). + + + + + + + + + + + +:::info[Multi Tenancy] + +For a multitenant setup, read the tenant ID from the email verification link's `tenantId` query parameter and replace +`public` in the request path. The public tenant also supports omitting the `/public` path segment. ::: The response body from the API call has a `status` property in it: + - `status: "OK"`: Email verification was successful. - `status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`: This can happen if the verification code expires or is invalid. You should ask the user to retry. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. + + - - -:::caution -- This API doesn't require an active session to succeed. +:::warning[- This API doesn't require an active session to succeed.] - If you are calling the above API on page load, there is an edge case in which email clients might open the verification link in the email (for scanning purposes) and consume the token in the URL. This would lead to issues in which an attacker could sign up using someone else’s email and end up with a verified status! To prevent this, on page load, you should check if a session exists, and if it does, only then call the above API. If a session does not exist, you should first show a button, which when clicked would call the above API (email clients do not automatically click on this button). The button text could be something like "Click here to verify your email". ::: - + ## References @@ -711,71 +580,69 @@ The response body from the API call has a `status` property in it: This is how the email that the user receives looks like: -UI of the verification email sent to the registered user +UI of the verification email sent to the registered user You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html) -To understand more about how you can customize the check the [email delivery](/docs/platform-configuration/email-delivery) section. +To understand more about how you can customize it, check the [email delivery](/platform-configuration/email-delivery) section. ### Verification link lifetime By default, the email verification link's lifetime is **1 day**. -This can change via a core's configuration (time in milliseconds): +This can change via the Core configuration (time in milliseconds): - - - - - Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. + + +- Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. - Open **Configuration** and find the **Email Verification** configuration card. - Change the `email_verification_token_lifetime` value. Configuration changes are saved automatically. - - - + + + + + + + ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ - // highlight-next-line - -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ + -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d supertokens/supertokens- ``` - - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` - - - + + ## Next steps - - -Change the email verification status manually - - -Generate email verification links manually - - -Protect routes based on the email verification status - - -Post email verification action - - -Customize the email delivery method - - -Session claim validators - - + + + + + + + + + + + + + + + + + + + + diff --git a/docs/additional-verification/email-verification/introduction.mdx b/docs/additional-verification/email-verification/introduction.mdx index 1198e3fd6f..ee4ffab4e5 100644 --- a/docs/additional-verification/email-verification/introduction.mdx +++ b/docs/additional-verification/email-verification/introduction.mdx @@ -1,19 +1,10 @@ --- -sidebar_position: 1 title: Introduction -hide_title: true -skip_llms_txt: true -description: >- - Use email verification to ensure user authenticity and protect application - routes. -page_type: overview -recipe: emailverification -category: email-verification +description: Use email verification to ensure user authenticity and protect application routes. +sidebar: + order: 10 --- - -# Email verification - ## Overview With the **Email Verification** recipe, you can confirm the email address of a user before they can use your application. @@ -22,48 +13,25 @@ With the **Email Verification** recipe, you can confirm the email address of a u You can go through the *Initial Setup* page for a quick tutorial on how to configure the feature. - - - - Quickstart - - - Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. - - - + + +Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. + + ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Protect frontend and backend routes - - - Limit the access to your frontend and backend routes to users who have confirmed their email address. - - - - - Manual actions - - - Generate verification links and change the email verification status manually. - - - - - Customize the pre-built UI - - - Learn how to change the look of the pre-built UI. - - - - - - + + +Limit the access to your frontend and backend routes to users who have confirmed their email address. + + +Generate verification links and change the email verification status manually. + + +Learn how to change the look of the pre-built UI. + + diff --git a/docs/additional-verification/email-verification/manual-actions.mdx b/docs/additional-verification/email-verification/manual-actions.mdx index afb29d6828..7f3c410286 100644 --- a/docs/additional-verification/email-verification/manual-actions.mdx +++ b/docs/additional-verification/email-verification/manual-actions.mdx @@ -1,18 +1,10 @@ --- title: Manual actions -hide_title: true -sidebar_position: 4 -description: >- - See how you can directly generate email verification links and set emails as - verified through manual actions. -page_type: guide -recipe: emailverification -category: email-verification +description: See how you can directly generate email verification links and set emails as verified through manual actions. +sidebar: + order: 40 --- - -# Manual actions - ## Overview Although the **SuperTokens** covers the entire email verification process you can also intervene manually in the process. @@ -24,32 +16,29 @@ The following page shows you what SDK methods you can use to adjust the verifica You can use the backend SDK to generate the email verification link as shown below: - - - + + ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function createEmailVerificationLink(recipeUserId: supertokens.RecipeUserId, email: string) { - try { - // Create an email verification link for the user - const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); - - if (linkResponse.status === "OK") { - console.log(linkResponse.link); - } else { - // user's email is already verified - } - } catch (err) { - console.error(err); + try { + // Create an email verification link for the user + const linkResponse = await EmailVerification.createEmailVerificationLink("public", recipeUserId, email); + + if (linkResponse.status === "OK") { + console.log(linkResponse.link); + } else { + // user's email is already verified } + } catch (err) { + console.error(err); + } } ``` - - - - + + ```go import ( "fmt" @@ -76,13 +65,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.emailverification.asyncio import create_email_verification_link from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationLinkOkResult @@ -98,9 +84,8 @@ async def create_link(recipe_user_id: RecipeUserId, email: str): else: print("user's email is already verified") ``` - - - + + ```python from supertokens_python.recipe.emailverification.syncio import create_email_verification_link from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationLinkOkResult @@ -116,14 +101,12 @@ def create_link(recipe_user_id: RecipeUserId, email: str): else: print("user's email is already verified") ``` + + + + - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that the first argument to the function call above is `"public"`. This refers to the default tenant ID that SuperTokens uses. It means that users belonging to the `"public"` tenant can only consume the generated email verification link. @@ -139,31 +122,28 @@ Finally, the generated link uses the configured `websiteDomain` from the `appInf To manually mark an email as verified, you need to first create an email verification token for the user and then use the token to verify the user's email. - - - + + ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function manuallyVerifyEmail(recipeUserId: supertokens.RecipeUserId) { - try { - // Create an email verification token for the user - const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); + try { + // Create an email verification token for the user + const tokenRes = await EmailVerification.createEmailVerificationToken("public", recipeUserId); - // If the token creation is successful, use the token to verify the user's email - if (tokenRes.status === "OK") { - await EmailVerification.verifyEmailUsingToken("public", tokenRes.token); - } - } catch (err) { - console.error(err); + // If the token creation is successful, use the token to verify the user's email + if (tokenRes.status === "OK") { + await EmailVerification.verifyEmailUsingToken("public", tokenRes.token); } + } catch (err) { + console.error(err); + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" @@ -186,13 +166,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.emailverification.asyncio import create_email_verification_token, verify_email_using_token from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult @@ -209,9 +186,8 @@ async def manually_verify_email(recipe_user_id: RecipeUserId): except Exception as e: print(e) ``` - - - + + ```python from supertokens_python.recipe.emailverification.syncio import create_email_verification_token, verify_email_using_token from supertokens_python.recipe.emailverification.interfaces import CreateEmailVerificationTokenOkResult @@ -228,14 +204,12 @@ def manually_verify_email(recipe_user_id: RecipeUserId): except Exception as e: print(e) ``` + + + + - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that the first argument of the function call above is `"public"`. This refers to the `"public"` `tenantId` (which is the default `tenantId`). In case you are using the multi tenancy feature, you can still pass in the `"public"` tenant ID here. Even if the user ID is not part of that tenant, you can pass it because the system creates and consumes the token in one shot. @@ -247,26 +221,23 @@ In case you are using the multi tenancy feature, you can still pass in the `"pub To manually mark an email as unverified, you need to first retrieve the user's email address and then update their email verification status in the database. - - - + + ```tsx import EmailVerification from "supertokens-node/recipe/emailverification"; import supertokens from "supertokens-node"; async function manuallyUnverifyEmail(recipeUserId: supertokens.RecipeUserId) { - try { - // Set email verification status to false - await EmailVerification.unverifyEmail(recipeUserId); - } catch (err) { - console.error(err); - } + try { + // Set email verification status to false + await EmailVerification.unverifyEmail(recipeUserId); + } catch (err) { + console.error(err); + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" @@ -281,13 +252,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.emailverification.asyncio import unverify_email from supertokens_python.types import RecipeUserId @@ -298,10 +266,8 @@ async def manually_unverify_email(recipe_user_id: RecipeUserId): except Exception as e: print(e) ``` - - - - + + ```python from supertokens_python.recipe.emailverification.syncio import unverify_email from supertokens_python.types import RecipeUserId @@ -313,13 +279,12 @@ def manually_unverify_email(recipe_user_id: RecipeUserId): except Exception as e: print(e) ``` - - - - - + + + + -:::info Multi Tenancy +:::info[Multi Tenancy] For a multi tenant setup, the function above does not take a tenant ID. A user ID and the associated email verification status is unique on an app level (and not a tenant level). ::: @@ -329,9 +294,9 @@ A user ID and the associated email verification status is unique on an app level ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/email-verification/meta.ts b/docs/additional-verification/email-verification/meta.ts new file mode 100644 index 0000000000..f2b50e814c --- /dev/null +++ b/docs/additional-verification/email-verification/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email Verification", + icon: "mail-check", + order: 2, +}); diff --git a/docs/additional-verification/email-verification/protecting-routes.mdx b/docs/additional-verification/email-verification/protecting-routes.mdx index 41b30f0ed4..e41f055bbe 100644 --- a/docs/additional-verification/email-verification/protecting-routes.mdx +++ b/docs/additional-verification/email-verification/protecting-routes.mdx @@ -1,19 +1,10 @@ --- title: Protect frontend and backend routes -hide_title: true -sidebar_position: 2 -description: >- - Protect API and website routes by implementing email verification checks for - secure access. -page_type: guide -recipe: emailverification -category: email-verification +description: Protect API and website routes by implementing email verification checks for secure access. +sidebar: + order: 30 --- - - -# Protecting backend and frontend routes - ## Overview The `EmailVerification` claim shows the status of the email verification process. @@ -21,12 +12,12 @@ Follow this page to understand how to limit access based on whether the user has ## Before you start - +:::info[Access token guidance] -If you are implementing [**Unified Login**](/docs/authentication/unified-login/introduction), you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. -Please read the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to verify the token. +If you are implementing [**Unified Login**](/authentication/unified-login/introduction), you must manually check the `email_verified` claim on the **OAuth2 Access Tokens**. +Please read the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify the token. - +::: --- @@ -37,34 +28,30 @@ Please read the [separate page](/docs/authentication/unified-login/verify-tokens If you want to protect all your backend API routes with email verification checks, set the `mode` to `REQUIRED` in the `EmailVerification` configuration. Routes protected with the `verifySession` middleware additionally check for email verification status. - - - + + ```tsx -import SuperTokens from 'supertokens-node'; +import SuperTokens from "supertokens-node"; import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - EmailVerification.init({ - // This means that verifySession will now only allow calls if the user has verified their email - mode: "REQUIRED", - }), - // highlight-end - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailVerification.init({ + // This means that verifySession will now only allow calls if the user has verified their email + mode: "REQUIRED", + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" @@ -77,21 +64,18 @@ import ( func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ - // highlight-start emailverification.Init(evmodels.TypeInput{ // This means that VerifySession will now only allow calls if the user has verified their email Mode: evmodels.ModeRequired, }), - // highlight-end session.Init(&sessmodels.TypeInput{}), }, }) } ``` - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe import emailverification @@ -99,27 +83,33 @@ from supertokens_python.recipe import emailverification init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ - # highlight-start # This means that VerifySession will now only allow calls if the user has verified their email emailverification.init(mode='REQUIRED'), - # highlight-end session.init() ] ) ``` - - - + + In case you have set the email verification mode to `REQUIRED` but want to disable the check for a specific route, you can make the following changes to the `verifySession` middleware: - - - - - + + + + + + + + + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; @@ -129,48 +119,46 @@ import { EmailVerificationClaim } from "supertokens-node/recipe/emailverificatio let app = express(); app.post( - "/update-blog", - verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), - }), - async (req: SessionRequest, res) => { - // All validator checks have passed and the user has a verified email address - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }), + async (req: SessionRequest, res) => { + // The session and remaining claim validators have passed; email verification was skipped + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), - }), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // All validator checks have passed and the user has a verified email address - } -}) + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // The session and remaining claim validators have passed; email verification was skipped + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -179,56 +167,58 @@ import { EmailVerificationClaim } from "supertokens-node/recipe/emailverificatio let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key), + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }), -}, async (req: SessionRequest, res) => { - // All validator checks have passed and the user has a verified email address -}); + }, + async (req: SessionRequest, res) => { + // The session and remaining claim validators have passed; email verification was skipped + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; async function updateBlog(awsEvent: SessionEvent) { - // All validator checks have passed and the user has a verified email address -}; + // The session and remaining claim validators have passed; email verification was skipped +} exports.handler = verifySession(updateBlog, { - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import {SessionContext} from "supertokens-node/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) - }), async (ctx: SessionContext, next) => { - // All validator checks have passed and the user has a verified email address -}); +router.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }), + async (ctx: SessionContext, next) => { + // The session and remaining claim validators have passed; email verification was skipped + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -237,112 +227,75 @@ import Session from "supertokens-node/recipe/session"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; class SetRole { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) - })) - @response(200) - async handler() { - // All validator checks have passed and the user has a verified email address - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }), + ) + @response(200) + async handler() { + // The session and remaining claim validators have passed; email verification was skipped + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -// highlight-start export default async function setRole(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) - })(req, res, next); - }, - req, - res - ) - // All validator checks have passed and the user has a verified email address -} -``` - - - - -```tsx -import SuperTokens from "supertokens-node"; -import { NextResponse, NextRequest } from "next/server"; -import { withSession } from "supertokens-node/nextjs"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export async function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // We skipped checking the email verification claim - return NextResponse.json({}); - }, - { - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) - } + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + })(req, res, next); + }, + req, + res, ); + // The session and remaining claim validators have passed; email verification was skipped } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => globalValidators.filter(v => v.id !== EmailVerificationClaim.key) - })) + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }), + ) async postExample(@Session() session: SessionContainer): Promise { - // All validator checks have passed and the user has a verified email address + // The session and remaining claim validators have passed; email verification was skipped return true; } } ``` - - - - - - - - + + + + + + ```go import ( "net/http" @@ -357,7 +310,6 @@ import ( func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { @@ -369,7 +321,6 @@ func main() { } return filtered, nil }, - // highlight-end }, exampleAPI).ServeHTTP(rw, r) }) } @@ -378,10 +329,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` - - - - + + ```go import ( "net/http" @@ -399,7 +348,6 @@ func main() { // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { @@ -411,7 +359,6 @@ func main() { } return filtered, nil }, - // highlight-end }), exampleAPI) } @@ -432,10 +379,8 @@ func exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` - - - - + + ```go import ( "net/http" @@ -453,7 +398,6 @@ func main() { // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { @@ -465,7 +409,6 @@ func main() { } return filtered, nil }, - // highlight-end }, exampleAPI)) } @@ -474,10 +417,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { } ``` - - - - + + ```go import ( "net/http" @@ -495,7 +436,6 @@ func main() { // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { filtered := []claims.SessionClaimValidator{} for _, v := range globalClaimValidators { @@ -507,7 +447,6 @@ func main() { } return filtered, nil }, - // highlight-end }, exampleAPI)).Methods(http.MethodPost) } @@ -515,91 +454,124 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] - # highlight-end ) )): - # All validator checks have passed and the user has a verified email address + # The session and remaining claim validators have passed; email verification was skipped pass ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] - # highlight-end ) def like_comment(): - # All validator checks have passed and the user has a verified email address + # The session and remaining claim validators have passed; email verification was skipped pass ``` - - - - + + ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.emailverification import EmailVerificationClaim @verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != EmailVerificationClaim.key] - # highlight-end ) async def like_comment(request: HttpRequest): - # All validator checks have passed and the user has a verified email address + # The session and remaining claim validators have passed; email verification was skipped pass ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import SuperTokens from "supertokens-node"; +import { NextResponse, NextRequest } from "next/server"; +import { withSession } from "supertokens-node/nextjs"; +import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); + +export async function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // We skipped checking the email verification claim + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async (globalValidators) => + globalValidators.filter((v) => v.id !== EmailVerificationClaim.key), + }, + ); +} +``` + + + + + + ### Add email verification checks to specific routes If you want to protect specific backend API routes with email verification checks, set the `mode` to `OPTIONAL` in the `EmailVerification` configuration. You then override the `verifySession` middleware protecting the route to check for email verification status. - - - - - + + + + + + + + + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; @@ -609,48 +581,50 @@ import { EmailVerificationClaim } from "supertokens-node/recipe/emailverificatio let app = express(); app.post( - "/update-blog", - verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], - }), - async (req: SessionRequest, res) => { - // All validator checks have passed and the user has a verified email address - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }), + async (req: SessionRequest, res) => { + // All validator checks have passed and the user has a verified email address + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], - }), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // All validator checks have passed and the user has a verified email address - } -}) + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // All validator checks have passed and the user has a verified email address + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -659,56 +633,64 @@ import { EmailVerificationClaim } from "supertokens-node/recipe/emailverificatio let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()], + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], }), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { // All validator checks have passed and the user has a verified email address -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; async function updateBlog(awsEvent: SessionEvent) { - // All validator checks have passed and the user has a verified email address -}; + // All validator checks have passed and the user has a verified email address +} exports.handler = verifySession(updateBlog, { - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import {SessionContext} from "supertokens-node/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] - }), async (ctx: SessionContext, next) => { +router.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }), + async (ctx: SessionContext, next) => { // All validator checks have passed and the user has a verified email address -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -717,112 +699,81 @@ import Session from "supertokens-node/recipe/session"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; class SetRole { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] - })) - @response(200) - async handler() { - // All validator checks have passed and the user has a verified email address - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }), + ) + @response(200) + async handler() { + // All validator checks have passed and the user has a verified email address + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -// highlight-start export default async function setRole(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] - })(req, res, next); - }, - req, - res - ) - // All validator checks have passed and the user has a verified email address -} -``` - - - - -```tsx -import SuperTokens from "supertokens-node"; -import { NextResponse, NextRequest } from "next/server"; -import { withSession } from "supertokens-node/nextjs"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export async function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // All validator checks have passed and the user has a verified email address - return NextResponse.json({}); - }, - { - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] - } + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + })(req, res, next); + }, + req, + res, ); + // All validator checks have passed and the user has a verified email address } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - // highlight-next-line - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [...globalValidators, EmailVerificationClaim.validators.isVerified()] - })) + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }), + ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user has a verified email address return true; } } ``` - - - - - - - - + + + + + + ```go import ( "net/http" @@ -837,12 +788,10 @@ import ( func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI).ServeHTTP(rw, r) }) } @@ -851,10 +800,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` - - - - + + ```go import ( "net/http" @@ -872,12 +819,10 @@ func main() { // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, - // highlight-end }), exampleAPI) } @@ -898,10 +843,8 @@ func exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` - - - - + + ```go import ( "net/http" @@ -919,12 +862,10 @@ func main() { // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)) } @@ -932,10 +873,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` - - - - + + ```go import ( "net/http" @@ -953,12 +892,10 @@ func main() { // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, evclaims.EmailVerificationClaimValidators.IsVerified(nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)).Methods(http.MethodPost) } @@ -966,202 +903,207 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( - # highlight-start # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] - # highlight-end ) )): # All validator checks have passed and the user has a verified email address pass ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.emailverification import EmailVerificationClaim -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session( - # highlight-start # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] - # highlight-end ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` - - - - + + ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.emailverification import EmailVerificationClaim @verify_session( - # highlight-start # We add the EmailVerificationClaim's is_verified validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [EmailVerificationClaim.validators.is_verified()] - # highlight-end ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import SuperTokens from "supertokens-node"; +import { NextResponse, NextRequest } from "next/server"; +import { withSession } from "supertokens-node/nextjs"; +import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); +export async function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // All validator checks have passed and the user has a verified email address + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + EmailVerificationClaim.validators.isVerified(), + ], + }, + ); +} +``` - + + + + + ---- -## Protect frontend routes + - +--- - +## Protect frontend routes - + + + ### Protect all frontend routes Set the email verification mode to `REQUIRED` and wrap your website routes using ``. -If the user's email is not verified, SuperTokens automatically redirects the user to the email verification screen. +If the user's email is not verified, SuperTokens automatically redirects the user to the email verification screen. ### Protect specific frontend routes Set the email verification mode to `OPTIONAL`. Create a generic component called `VerifiedRoute` which enforces that its child components can only render if the user has a verified email address. + + + + ```tsx import React from "react"; -import { SessionAuth, useSessionContext } from 'supertokens-auth-react/recipe/session'; -import { EmailVerificationClaim } from 'supertokens-auth-react/recipe/emailverification'; - -const VerifiedRoute = (props: React.PropsWithChildren) => { - return ( - - - {props.children} - - - ); -} +import { SessionAuth, useSessionContext } from "supertokens-auth-react/recipe/session"; +import { EmailVerificationClaim } from "supertokens-auth-react/recipe/emailverification"; + +const VerifiedRoute = (props: React.PropsWithChildren) => { + return ( + + {props.children} + + ); +}; -function InvalidClaimHandler(props: React.PropsWithChildren) { - let sessionContext = useSessionContext(); - if (sessionContext.loading) { - return null; - } +function InvalidClaimHandler(props: React.PropsWithChildren) { + let sessionContext = useSessionContext(); + if (sessionContext.loading) { + return null; + } - if (sessionContext.invalidClaims.some(i => i.id === EmailVerificationClaim.id)) { - // Alternatively you could redirect the user to the email verification screen to trigger the verification email - // Note: /auth/verify-email is the default email verification path - // window.location.assign("/auth/verify-email") - return
You cannot access this page because your email address is not verified.
- } + if (sessionContext.invalidClaims.some((i) => i.id === EmailVerificationClaim.id)) { + // Alternatively you could redirect the user to the email verification screen to trigger the verification email + // Note: /auth/verify-email is the default email verification path + // window.location.assign("/auth/verify-email") + return
You cannot access this page because your email address is not verified.
; + } - // We show the protected route since all claims validators have - // passed implying that the user has verified their email. - return
{props.children}
; + // We show the protected route since all claims validators have + // passed implying that the user has verified their email. + return
{props.children}
; } ``` -In the `VerifiedRoute` component, use the `SessionAuth` wrapper to ensure that the session exists. -The `` component automatically adds the `EmailVerificationClaim` validator if you initialize the `EmailVerification` recipe. - -Finally, check the result of the validation in the `InvalidClaimHandler` component which displays `"You cannot access this page because your email address is not verified. -"` if the `EmailVerificationClaim` validator failed. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email. - -:::note -You can extend the `VerifiedRoute` component to check for other types of validators as well. -This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). -::: - -### Check the verification status manually - -If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: - -```tsx -import Session from "supertokens-auth-react/recipe/session"; -import {EmailVerificationClaim} from "supertokens-auth-react/recipe/emailverification" - -function ProtectedComponent() { - let claimValue = Session.useClaimValue(EmailVerificationClaim) - if (claimValue.loading || !claimValue.doesSessionExist) { - return null; - } - let isEmailVerified = claimValue.value; - if (isEmailVerified !== undefined && isEmailVerified) { - //... - } else { - // Redirect the user the email verification path to send the verification email - // Note: /auth/verify-email is the default email verification path - window.location.assign("/auth/verify-email") - } -} -``` - - - -
- - - + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - let validationErrors = await Session.validateClaims(); + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); - if (validationErrors.length === 0) { - // user has verified their email address - return true; - } else { - for (const err of validationErrors) { - if (err.id === EmailVerificationClaim.id) { - // email is not verified - } - } + if (validationErrors.length === 0) { + // user has verified their email address + return true; + } else { + for (const err of validationErrors) { + if (err.id === EmailVerificationClaim.id) { + // email is not verified } + } } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` + + + + +In the `VerifiedRoute` component, use the `SessionAuth` wrapper to ensure that the session exists. +The `` component automatically adds the `EmailVerificationClaim` validator if you initialize the `EmailVerification` recipe. + +Finally, check the validation result in `InvalidClaimHandler`. It displays `"You cannot access this page because your email address is not verified."` if the `EmailVerificationClaim` validator failed. Alternatively you could also redirect the user to the default email verification path to trigger the sending of the verification email. + +:::note[You can extend the `VerifiedRoute` component to check for other types of validators as well.] +You can reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). +::: + +### Check the verification status manually + +If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: + + In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it reflects in the `validationErrors` variable. @@ -1170,257 +1112,163 @@ The `EmailVerificationClaim` validator is automatically checked by this function ### Validation errors In case the `validationErrors` array is not empty, you can loop through the errors to know which claim has failed: + + + + + +```tsx +import Session from "supertokens-auth-react/recipe/session"; +import { EmailVerificationClaim } from "supertokens-auth-react/recipe/emailverification"; +function ProtectedComponent() { + let claimValue = Session.useClaimValue(EmailVerificationClaim); + if (claimValue.loading || !claimValue.doesSessionExist) { + return null; + } + let isEmailVerified = claimValue.value; + if (isEmailVerified !== undefined && isEmailVerified) { + //... + } else { + // Redirect the user the email verification path to send the verification email + // Note: /auth/verify-email is the default email verification path + window.location.assign("/auth/verify-email"); + } +} +``` + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute() { - let validationErrors = await Session.validateClaims(/*{...}*/); - // highlight-start - for (const err of validationErrors) { - if (err.id === EmailVerificationClaim.id) { - // email verification claim check failed - } else { - // some other claim check failed (from the global validators list) - } + let validationErrors = await Session.validateClaims(/*{...}*/); + for (const err of validationErrors) { + if (err.id === EmailVerificationClaim.id) { + // email verification claim check failed + } else { + // some other claim check failed (from the global validators list) } - // highlight-end + } } ``` + + - - + + ### Check the verification status manually If you want to have more complex access control, you can either create your own validator, or you can get the boolean from the session as follows. Check it yourself: + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let isVerified = await Session.getClaimValue({claim: EmailVerificationClaim}); - if (isVerified) { - // user has verified their email address - return true; - } - // highlight-end + if (await Session.doesSessionExist()) { + let isVerified = await Session.getClaimValue({ claim: EmailVerificationClaim }); + if (isVerified) { + // user has verified their email address + return true; } - // either a session does not exist, or the user has not verified their email address - return false + } + // either a session does not exist, or the user has not verified their email address + return false; } ``` + + +
+ - - - - -
- - ## Protect frontend routes - - - + - - - + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { EmailVerificationClaim, sendVerificationEmail } from "supertokens-web-js/recipe/emailverification"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let validationErrors = await Session.validateClaims(); - - if (validationErrors.length === 0) { - // user has verified their email address - return true; - } else { - for (const err of validationErrors) { - if (err.id === EmailVerificationClaim.id) { - // email is not verified - // Send the verification email to the user - await sendEmail(); - } - } - } - // highlight-end - } - // a session does not exist, or email is not verified - return false -} + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); -async function sendEmail() { - try { - let response = await sendVerificationEmail(); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - // This can happen if the info about email verification in the session was outdated. - // Redirect the user to the home page - window.location.assign("/home"); - } else { - // email was sent successfully. - window.alert("Please check your email and click the link in it") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} - -``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensEmailVerification from "supertokens-web-js-script/recipe/emailverification"; -async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - // highlight-start - let validationErrors = await supertokensSession.validateClaims(); - - if (validationErrors.length === 0) { - // user has verified their email address - return true; - } else { - for (const err of validationErrors) { - if (err.id === supertokensEmailVerification.EmailVerificationClaim.id) { - // email is not verified - // Send the verification email to the user - await sendEmail(); - } - } + if (validationErrors.length === 0) { + // user has verified their email address + return true; + } else { + for (const err of validationErrors) { + if (err.id === EmailVerificationClaim.id) { + // email is not verified + // Send the verification email to the user + await sendEmail(); } - // highlight-end + } } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } async function sendEmail() { - try { - let response = await supertokensEmailVerification.sendVerificationEmail(); - if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { - // This can happen if the info about email verification in the session was outdated. - // Redirect the user to the home page - window.location.assign("/home"); - } else { - // email was sent successfully. - window.alert("Please check your email and click the link in it") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + try { + let response = await sendVerificationEmail(); + if (response.status === "EMAIL_ALREADY_VERIFIED_ERROR") { + // This can happen if the info about email verification in the session was outdated. + // Redirect the user to the home page + window.location.assign("/home"); + } else { + // email was sent successfully. + window.alert("Please check your email and click the link in it"); } -} -``` - - - -:::note -The API for sending an email verification email requires an active session. -If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. -::: - - - -In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. -This function inspects the session's contents and runs claim validators on them. -If a claim validator fails, it reflects in the `validationErrors` variable. -The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. - - -### Handle 403 responses on the frontend - - - -If your frontend queries a protected API on your backend and it fails with a 403, you can call the `validateClaims` function. Loop through the errors to know which claim has failed: - -```tsx -import axios from "axios"; -import Session from "supertokens-web-js/recipe/session"; -import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; - -async function callProtectedRoute() { - try { - let response = await axios.get("^{appInfo.apiDomain}/protectedroute"); - } catch (error) { - // highlight-start - if (axios.isAxiosError(error) && error.response?.status === 403) { - let validationErrors = await Session.validateClaims(); - for (let err of validationErrors) { - if (err.id === EmailVerificationClaim.id) { - // email verification claim check failed - // We call the sendEmail function defined in the previous section to send the verification email. - // await sendEmail(); - } else { - // some other claim check failed (from the global validators list) - } - } - // highlight-end - - } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); } + } } ``` - - - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function checkIfEmailIsVerified() { - if (await SuperTokens.doesSessionExist()) { - - // highlight-start - let isVerified: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-ev"].v; + if (await SuperTokens.doesSessionExist()) { + let isVerified: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-ev"].v; - if (isVerified) { - // TODO.. - } else { - // You can trigger the sending of the verification email by calling `^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify/token` - } - // highlight-end + if (isVerified) { + // TODO.. + } else { + // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -1433,16 +1281,13 @@ class MainApplication: Application() { if (isVerified) { // TODO.. } else { - // You can trigger the sending of the verification email by calling `^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify/token` + // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -1453,17 +1298,14 @@ fileprivate class ViewController: UIViewController { if isVerified { // Email is verified } else { - // You can trigger the sending of the verification email by calling `^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify/token` + // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -1479,34 +1321,79 @@ Future checkIfEmailIsVerified() async { if (isVerified) { // Email is verified } else { - // You can trigger the sending of the verification email by calling `^{appInfo.apiDomain}^{appInfo.apiBasePath}/user/email/verify/token` + // You can trigger the sending of the verification email by calling `/auth/user/email/verify/token` } } } } ``` + + + + + + + +:::note[The API for sending an email verification email requires an active session.] +If you are using the frontend SDKs, then the session tokens should automatically get attached to the request. +::: - +In your protected routes, you need to first check if a session exists, and then call the `Session.validateClaims` function as shown above. +This function inspects the session's contents and runs claim validators on them. +If a claim validator fails, it reflects in the `validationErrors` variable. +The `EmailVerificationClaim` validator is automatically checked by this function since you have initialized the email verification recipe. - +### Handle 403 responses on the frontend +If your frontend queries a protected API on your backend and it fails with a 403, you can call the `validateClaims` function. Loop through the errors to know which claim has failed: + + ### Handle 403 responses on the frontend If your frontend queries a protected API on your backend and it fails with a 403, you can check the value of the `st-ev` claim in the access token payload. -If it is false you can send the verification email +If it is `false`, you can send the verification email. + + + + + +```tsx +import axios from "axios"; +import Session from "supertokens-web-js/recipe/session"; +import { EmailVerificationClaim } from "supertokens-web-js/recipe/emailverification"; + +async function callProtectedRoute() { + try { + let response = await axios.get("/protectedroute"); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 403) { + let validationErrors = await Session.validateClaims(); + for (let err of validationErrors) { + if (err.id === EmailVerificationClaim.id) { + // email verification claim check failed + // We call the sendEmail function defined in the previous section to send the verification email. + // await sendEmail(); + } else { + // some other claim check failed (from the global validators list) + } + } + } + } +} +``` + + - - - + --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/meta.ts b/docs/additional-verification/meta.ts new file mode 100644 index 0000000000..4f7d18a640 --- /dev/null +++ b/docs/additional-verification/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Additional Verification", + icon: "shield-check", + order: 50, + collapsed: true, +}); diff --git a/docs/additional-verification/mfa/_blocks/paid-feature-callout.mdx b/docs/additional-verification/mfa/_blocks/paid-feature-callout.mdx deleted file mode 100644 index 96a38808a7..0000000000 --- a/docs/additional-verification/mfa/_blocks/paid-feature-callout.mdx +++ /dev/null @@ -1,8 +0,0 @@ - -:::warning Paid Feature -This is a paid feature. - -For self hosted users, [Sign up](https://supertokens.com/auth) to get a license key and follow the instructions sent to you by email. Using the `dev` license key is free. Charging only starts once you enable the feature in production using the provided production license key. - -For managed service users, open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and enable this feature from **Features**. Changes are saved automatically. Once enabled, this feature is free on the provided development environment. -::: diff --git a/docs/additional-verification/mfa/_category_.json b/docs/additional-verification/mfa/_category_.json deleted file mode 100644 index 3f68d29e7c..0000000000 --- a/docs/additional-verification/mfa/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Multi Factor Authentication", - "position": 1.2 -} diff --git a/docs/additional-verification/mfa/backup-codes.mdx b/docs/additional-verification/mfa/backup-codes.mdx index caa2311cc7..90f682621d 100644 --- a/docs/additional-verification/mfa/backup-codes.mdx +++ b/docs/additional-verification/mfa/backup-codes.mdx @@ -1,25 +1,16 @@ --- title: Implement recovery codes -hide_title: true -sidebar_position: 7 -description: >- - Implement backup codes for account recovery by customizing SuperTokens SDKs - for MFA users. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement backup codes for account recovery by customizing SuperTokens SDKs for MFA users. +sidebar: + order: 70 --- - -# Implement recovery codes - - ## Overview Backup codes is one of the ways in which end users can recover their account in case they lose their second factor device. At the moment, SuperTokens does not have an in-built implementation for backup codes, however, you can customize the SDKs to add it. -:::info Note +:::info[Note] [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes) of how you can implement backup codes in your application if you are using the pre-built UI. ::: @@ -37,7 +28,7 @@ User can then go about adding a new device and completing MFA using that. These instructions assume that you already have some knowledge of MFA. -If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). +If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). The guide below focuses on Time-based One-Time Password (TOTP) as a second factor, but you can implement something similar for Passwordless as well. @@ -51,9 +42,9 @@ This API returns the plain text recovery code to the frontend to display to the ### 2. Allow users to generate a backup code when they finish MFA setup - + - + After the user has successfully set up their second factor (during sign up or during recovery process), they navigate to a page which shows them their backup code. @@ -74,9 +65,9 @@ Note that calling this API replaces the older recovery code, but since it's all [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. - + - + For custom UI, the UI for where and how you show the recovery code page is up to you. It is advisable to show the user this page post sign up, or whenever they create a new MFA device successfully. @@ -97,12 +88,12 @@ Note that calling this API replaces the older recovery code, but since it's all [Here is an example](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/CreateRecoveryCode/index.tsx) of how to implement this page. - + ### 3. Show how to use backup codes on the MFA challenge UI - + You can achieve this by creating a "Lost device?" button in the pre-built UI that asks the user to enter the Time-based One-Time Password (TOTP) challenge. Once they click on this, users redirect to a page where they can enter their backup code. After verification, they further redirect to the create a new Time-based One-Time Password (TOTP) device page. @@ -111,14 +102,14 @@ Once they click on this, users redirect to a page where they can enter their bac [Here is an example implementation](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx) of a page which asks the user to enter their backup code. It then calls an API (see next step) to check if the code is correct or not. - + - + You should make a UI that asks the user to enter their backup code and call the API to verify it and mark it as "in use" (see next step). You want to give the option for users to enter their backup code when asked for the MFA challenge. - + ### 4. Modify the user's session to mark that they have verified their backup code @@ -126,18 +117,18 @@ On the backend, you set up an API that accepts the recovery code entered by the You achieve this by saving the `recoverCodeHash` in the session payload, which is then checked in the next step to force enable Time-based One-Time Password (TOTP) device creation. - + On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. You can see how this process completes in the [index.tsx file](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-multifactorauth-recovery-codes/frontend/src/RecoveryCode/index.tsx#L20). - + - + On the frontend, once this API returns a success, the user should navigate to the create a new Time-based One-Time Password (TOTP) device screen. - + ### 5. Force users to setup a new device @@ -162,11 +153,9 @@ Finally, after successfully creating a new Time-based One-Time Password (TOTP) d ## See also - - - - - - - - + + + + + + diff --git a/docs/additional-verification/mfa/email-sms-otp/_category_.json b/docs/additional-verification/mfa/email-sms-otp/_category_.json deleted file mode 100644 index 35f798b184..0000000000 --- a/docs/additional-verification/mfa/email-sms-otp/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "OTP", - "position": 6 -} diff --git a/docs/additional-verification/mfa/email-sms-otp/meta.ts b/docs/additional-verification/mfa/email-sms-otp/meta.ts new file mode 100644 index 0000000000..af0f81856a --- /dev/null +++ b/docs/additional-verification/mfa/email-sms-otp/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "OTP", + icon: "smartphone", + order: 50, +}); diff --git a/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users.mdx b/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users.mdx index 73e4eca714..fadded498e 100644 --- a/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users.mdx +++ b/docs/additional-verification/mfa/email-sms-otp/otp-for-all-users.mdx @@ -1,126 +1,125 @@ --- -id: otp-for-all-users title: OTP required for all users -hide_title: true -sidebar_position: 1 -description: >- - Implement a multi-factor authentication policy requiring all users to complete - an OTP challenge. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement a multi-factor authentication policy requiring all users to complete an OTP challenge. +sidebar: + order: 1 --- - -# OTP required for all users +## Overview This page shows how to implement an MFA policy that requires all users to complete an OTP challenge before accessing your application. The OTP can be sent via email or phone. -:::note -Assume that the first factor is email password or social login, but the same set of steps applies to other first factor types as well. -::: + + ## Single tenant setup ### Backend setup To start with, configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - // highlight-end - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - return [MultiFactorAuth.FactorIds.OTP_EMAIL] - } - } - } - } - // highlight-end - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [MultiFactorAuth.FactorIds.OTP_EMAIL]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig, MFARequirementList from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer @@ -150,7 +149,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -166,7 +165,6 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-next-line return [FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( @@ -184,41 +182,50 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), + accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), - accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - - - + + - Notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, with `contactMethod` set to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, `otp`). If you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. -- We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/docs/post-authentication/account-linking/automatic-account-linking). -- Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can link to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then you can set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. +- We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/post-authentication/account-linking/automatic-account-linking). +- `shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. - The `getMFARequirementsForAuth` function is overridden to indicate that `otp-email` must be completed before the user can access the app. Notice that `userId` is not checked there, and `otp-email` is returned for all users. You can also return `otp-phone` instead if you want users to complete the OTP challenge via a phone SMS. Finally, if you want to give users an option for email or phone, you can return the following array from the function: ```json - [{ + [ + { "oneOf": ["otp-email", "otp-phone"] - }] + } + ] ``` Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -226,152 +233,145 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "otp-email": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } } ``` -Indicating that the user has finished all required factors, and should be allowed to access the app. +This indicates that the user has finished all required factors and should be allowed to access the app. -:::caution -If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly. +:::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly.] ::: ### Frontend setup - + - + We start by modifying the `init` function call on the frontend like this: - + + +You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import ThirdParty from "supertokens-auth-react/recipe/thirdparty" -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" -import Passwordless from "supertokens-auth-react/recipe/passwordless" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import supertokens from "supertokens-auth-react"; +import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - ThirdParty.init(/* ... */), - EmailPassword.init( /* ... */), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL" - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init(/* ... */), + EmailPassword.init(/* ... */), + Passwordless.init({ + contactMethod: "EMAIL", + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` - - - - - -You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty" -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless" -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIThirdParty.init(/* ... */), - supertokensUIEmailPassword.init( /* ... */), - // highlight-start - supertokensUIPasswordless.init({ - contactMethod: "EMAIL" - }), - supertokensUIMultiFactorAuth.init({ - firstFactors: [ - supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, - supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIThirdParty.init(/* ... */), + supertokensUIEmailPassword.init(/* ... */), + supertokensUIPasswordless.init({ + contactMethod: "EMAIL", + }), + supertokensUIMultiFactorAuth.init({ + firstFactors: [ + supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, + supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, + ], + }), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - Passwordless.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + Passwordless.init(), + ], }); ``` - - - - + + - Like on the backend, the `passwordless` recipe initializes in the `recipeList`. The `contactMethod` needs to be consistent with the backend setting. - The `MultiFactorAuth` recipe is also initialized, and the first factors to use are included. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, add the Passwordless pre-built UI when rendering the SuperTokens component: - - - - - - + + +:::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] +::: + + + + + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; @@ -382,9 +382,12 @@ function App() {
- // highlight-start - {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-end + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ + EmailPasswordPreBuiltUI, + ThirdPartyPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} // ... other routes
@@ -394,256 +397,245 @@ function App() { ); } ``` - -
- - - + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; function App() { - // highlight-start - if (canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])) { - return getRoutingComponent([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) + if ( + canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) + ) { + return getRoutingComponent([ + EmailPasswordPreBuiltUI, + ThirdPartyPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ]); } - // highlight-end - return ( - {/*Your app*/} - ); + return {/*Your app*/}; } ``` + + + + - - -
- -
- - - -:::success -This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. -::: - - - -
+ + With the above configuration, users see `emailpassword` or social login UI when they visit the auth page. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). -:::caution -If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly. +:::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly.] ::: -
+ - + -We start by initializing the MFA and Passwordless recipe on the frontend like this: - +We start by initializing the MFA and Passwordless recipe on the frontend like this: - - - + + +:::success[This step is not applicable for mobile apps. Please continue reading.] +::: + + + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - Passwordless.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + Passwordless.init(), + ], }); ``` - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - supertokensMultiFactorAuth.init(), - supertokensPasswordless.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + supertokensMultiFactorAuth.init(), + supertokensPasswordless.init(), + ], }); ``` + + + + - - + + - - - - -:::success -This step is not applicable for mobile apps. Please continue reading. -::: +After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](/additional-verification/mfa/initial-setup#12-add-the-mfa-flow). If it's not, then the user can redirect to the application page. - - - - -After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then the user can redirect to the application page. - -If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should complete next. Based on the backend configuration in this page, the `next` array contains `["otp-email"]`. +If it's `false`, the frontend then needs to [call the MFA endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) to get information about which factor the user should complete next. Based on the backend configuration in this page, the `next` array contains `["otp-email"]`. Two possibilities exist here: - Case 1: The user needs to set up an email to send the OTP to. This only happens if the first factor doesn't provide an email from the user (for example, if you used phone-based `otp` as the first factor). In this example on this doc, an email is always obtained from the first factor, so you do not need to build UI for this step (but this will still be discussed later on). -- Case 2: The user already has an email associated with them and need to complete the OTP challenge. +- Case 2: The user already has an email associated with them and needs to complete the OTP challenge. -We can know which case it is by checking if the `emails` object returned from [MFA Info endpoint](../frontend-setup#mfa-info-endpoint) contains any emails associated with the `otp-email` key. If the `emails["otp-email"]` property of the response is `undefined` or an empty array, then it's case 1, else it's case 2. +We can know which case it is by checking if the `emails` object returned from [MFA Info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) contains any emails associated with the `otp-email` key. If the `emails["otp-email"]` property of the response is `undefined` or an empty array, then it's case 1, else it's case 2. #### Case 1 implementation: User needs to enter their email -In this case, a form needs to be created wherein the user can enter their email. Once they submit the form, the [`createCode` API](/docs/passwordless/custom-ui/login-otp) needs to be called. +In this case, a form needs to be created wherein the user can enter their email. Once they submit the form, the [`createCode` API](/authentication/passwordless/initial-setup#21-creating-and-sending-the-otp) needs to be called. -After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by asking the user to enter their email once again and then call the `createCode` function. +After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/authentication/passwordless/initial-setup#23-verifying-the-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by asking the user to enter their email once again and then call the `createCode` function. #### Case 2 implementation: User needs to complete the OTP challenge -This case is when the user already has an email associated with their account and you can directly send a code to that email. You can get the email to send the code to from the result of the [MFA Info endpoint](../frontend-setup#mfa-info-endpoint). Specifically, from the response, you can read the email from the `emails` property like this: `emails["otp-email"][0]`. The first item in the array of email is picked since the emails are ordered based on: +This case is when the user already has an email associated with their account and you can directly send a code to that email. You can get the email to send the code to from the result of the [MFA Info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). Specifically, from the response, you can read the email from the `emails` property like this: `emails["otp-email"][0]`. The first item in the array of emails is picked since the emails are ordered based on: - Index 0 contains the email that belongs to the session's user. If the user's first factor was email password, the email in the 0th index of the array is that email. - The other emails in the array (if they exist), are from other login methods for this user ordered based on the oldest login method first. -You can even show a UI here asking the user to pick an email from the array if you like. Either way, when you have an email, you can call the [`createCode` API](/docs/passwordless/custom-ui/login-otp) to send the code to that email. +You can even show a UI here asking the user to pick an email from the array if you like. Either way, when you have an email, you can call the [`createCode` API](/authentication/passwordless/initial-setup#21-creating-and-sending-the-otp) to send the code to that email. -After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/docs/passwordless/custom-ui/login-otp#step-3-verifying-the-input-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by calling the `createCode` function once again in the background. +After this API call, you can show the user the enter OTP screen, and call the [`consumeCode` API](/authentication/passwordless/initial-setup#23-verifying-the-otp). If the API call returns a `RESTART_FLOW_ERROR`, you can handle this by calling the `createCode` function once again in the background. -:::note -Notice that in Case 2, there is no UI for the user to enter an email. That happens. The user only sees the enter OTP screen. +:::note[In Case 2, there is no UI for the user to enter an email. The user only sees the enter OTP screen.] ::: We recommend that you add a sign out button when showing the second factor (case 1 or case 2) so that users can use this to escape out of the flow in case they are unable to complete the second factor. When the sign out button is clicked, you want to: - Call the `await clearLoginAttemptInfo()` function (if on web) to clear the state that's set in the browser storage when calling the `createCode` function. - Call the sign out function / API to clear the tokens. -On successful verification of the code, the `otp-email` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). +On successful verification of the code, the `otp-email` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). - + ## Multi tenant setup -In a multi-tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling OTP for all users within specific tenants. +In a multi-tenancy setup, you may want to enable email / phone OTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#backend-setup) section above, so in this section, we will focus on enabling OTP for all users within specific tenants. ### Backend setup To start, initialize the Passwordless and the MultiFactorAuth recipes in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; import AccountLinking from "supertokens-node/recipe/accountlinking"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - MultiFactorAuth.init() - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init(), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, @@ -671,7 +663,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -685,62 +677,70 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), + accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), multifactorauth.init(), - accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - - - + + Unlike the single tenant setup, no configuration is provided to the `MultiFactorAuth` recipe because all the necessary configuration is done on a tenant level. - - - - + + +To configure otp-email requirement for a tenant, the following API can be called: + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + To configure otp-email requirement for a tenant, the following API can be called: + + +Enable EmailPassword and ThirdParty, OTP-Email +As shown above, enable **Email Password** and **Third Party** in the Login methods section and enable **OTP - Email** in the Secondary Factors Section. + + + + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } } ``` + + -- In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. -- The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -763,10 +763,8 @@ async def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -790,16 +788,10 @@ def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - - - - - -To configure otp-email requirement for a tenant, the following API can be called: - + + + + ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ @@ -810,32 +802,33 @@ curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/ "requiredSecondaryFactors": ["otp-email"] }' ``` + + + + + + + - In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. - The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. - - - - - -Enable EmailPassword and ThirdParty, OTP-Email - -As shown above, enable **Email Password** and **Third Party** in the Login methods section and enable **OTP - Email** in the Secondary Factors Section. - - - - + + +- In the above, the `firstFactors` are set to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. +- The `requiredSecondaryFactors` is set to `["otp-email"]` to indicate that OTP email is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. + + Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this: ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -843,158 +836,141 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "otp-email": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } } ``` -Indicating that the user has finished all required factors, and should be allowed to access the app. +This indicates that the user has finished all required factors and should be allowed to access the app. -:::caution -If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly. +:::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` and `flowType` are set correctly.] ::: ### Frontend setup - + We start by modifying the `init` function call on the frontend like this: - + + +You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import ThirdParty from "supertokens-auth-react/recipe/thirdparty" -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import Passwordless from "supertokens-auth-react/recipe/passwordless" -import Multitenancy from "supertokens-auth-react/recipe/multitenancy" +import supertokens from "supertokens-auth-react"; +import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL" - }), - MultiFactorAuth.init(), - Multitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + }), + MultiFactorAuth.init(), + Multitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); ``` - - - - - -You have to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty" -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless" -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - supertokensUIThirdParty.init({ - //... - }), - supertokensUIEmailPassword.init({ - //... - }), - // highlight-start - supertokensUIPasswordless.init({ - contactMethod: "EMAIL" - }), - supertokensUIMultiFactorAuth.init(), - supertokensUIMultitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + supertokensUIThirdParty.init({ + //... + }), + supertokensUIEmailPassword.init({ + //... + }), + supertokensUIPasswordless.init({ + contactMethod: "EMAIL", + }), + supertokensUIMultiFactorAuth.init(), + supertokensUIMultitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + -```tsx -import supertokens from "supertokens-web-js-script"; -import MultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import Session from "supertokens-web-js-script/recipe/session"; - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // highlight-start - Session.init(), - MultiFactorAuth.init(), - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - - - - + + - Like on the backend, the `Passwordless` recipe initializes in the `recipeList`. Make sure that the configuration for it is consistent with what's on the backend. - The `MultiFactorAuth` recipe is also initialized. Notice that unlike the single tenant setup, the `firstFactors` are not specified here. That information is fetched based on the `tenantId` you provide the SDK with. @@ -1003,20 +979,23 @@ supertokens.init({ Next, add the Passwordless pre-built UI when rendering the SuperTokens component: - - - - - - + + +:::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] +::: + + + + + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; @@ -1027,9 +1006,12 @@ function App() {
- // highlight-start - {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-end + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ + EmailPasswordPreBuiltUI, + ThirdPartyPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} // ... other routes
@@ -1039,70 +1021,57 @@ function App() { ); } ``` - -
- - - + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; function App() { - // highlight-start - if (canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])) { - return getRoutingComponent([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) + if ( + canHandleRoute([EmailPasswordPreBuiltUI, ThirdPartyPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI]) + ) { + return getRoutingComponent([ + EmailPasswordPreBuiltUI, + ThirdPartyPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ]); } - // highlight-end - return ( - {/*Your app*/} - ); + return {/*Your app*/}; } ``` + + + + - - -
- -
- - - -:::success -This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. -::: - - - -
+ + With the above configuration, users see the first and second factor based on the tenant configuration. For the tenant configured above, users see email password or social login first. After completing that, users redirect to `/auth/mfa/otp-email` (assuming that the `websiteBasePath` is `/auth`) where they are asked to complete the OTP challenge. The UI for this screen looks like: - [Factor Setup UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--setup-email) (This is in case the first factor doesn't provide an email for the user. In this example, the first factor does provide an email since it's email password or social login). - [Verification UI](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/passwordless-mfa--verification). -:::caution -If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly. +:::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. Ensure that the `contactMethod` is set correctly.] ::: -
+ - + The steps here are the same as in [the single tenant setup above](#frontend-setup). - - -## Protecting frontend and backend routes - -See the section on [protecting frontend and backend routes](../protect-routes). + -## Email / SMS sending and design -By default, the email template used for otp-email login is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and SMS sending service that is provided. +## See also -If you would like to learn more about this, or change the content of the email, or the method by which they are sent, checkout the email / SMS delivery section in the recipe docs: -- [Email delivery configuration](/docs/platform-configuration/email-delivery) -- [SMS delivery configuration](/docs/platform-configuration/sms-delivery) + + + + + diff --git a/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users.mdx b/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users.mdx index 225f260ac4..2eeca7e493 100644 --- a/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users.mdx +++ b/docs/additional-verification/mfa/email-sms-otp/otp-for-opt-in-users.mdx @@ -1,33 +1,22 @@ --- -id: otp-for-opt-in-users title: OTP for specific users -hide_title: true -sidebar_position: 2 -description: >- - Implement MFA policy for specific users using OTP via email or SMS based on - defined criteria. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement MFA policy for specific users using OTP via email or SMS based on defined criteria. +sidebar: + order: 2 --- - -# OTP for specific users - -:::important +:::note Before reading the below, please first go through the setup for [OTP for all users](./otp-for-all-users) to understand the basics of how MFA with OTP works, and then come back here. ::: This page shows how to implement an MFA policy that requires certain users to do the OTP challenge via email or SMS. You can decide which users based on any criteria. For example: -- Only users that have an `admin` role require to do OTP; OR +- Only users that have an `admin` role are required to complete OTP; OR - Only users that have enabled OTP on their account require to do OTP; OR - Only users that have a paid account require to do OTP. -Whatever the criteria is, the steps to implementing this type of a flow is the same. +Whatever the criteria, the steps for implementing this type of flow are the same. -:::note -Assume that the first factor is email password or social login, but the same set of steps applies to other first factor types as well. -::: + ## Single tenant setup @@ -38,110 +27,119 @@ Assume that the first factor is email password or social login, but the same set To start with, configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" -import UserRoles from "supertokens-node/recipe/userroles" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - UserRoles.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) - if (roles.roles.includes("admin")) { - // we only want otp-email for admins - return [MultiFactorAuth.FactorIds.OTP_EMAIL] - } else { - // no MFA for non admin users. - return [] - } - } - } - } - } - // highlight-end - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); + if (roles.roles.includes("admin")) { + // we only want otp-email for admins + return [MultiFactorAuth.FactorIds.OTP_EMAIL]; + } else { + // no MFA for non-admin users. + return []; + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, + userroles, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, @@ -176,7 +174,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -217,139 +215,152 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ - multifactorauth.init( - first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], - override=OverrideConfig(functions=override_functions), + session.init(), + userroles.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), + multifactorauth.init( + first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], + override=OverrideConfig(functions=override_functions), + ), ], ) ``` - - - + + Override the `getMFARequirementsForAuth` function to indicate that `otp-email` applies only to users with the `admin` role. You can also have any other criteria here. - - - #### Example 2: Ask for OTP only for users that have enabled OTP on their account To start with, configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - override: { - apis: (oI) => { - return { - ...oI, - consumeCodePOST: async function (input) { - let response = await oI.consumeCodePOST!(input); - if (response.status === "OK" && input.session !== undefined) { - // We do this only if a session exists, which means that it's not being called for first factor login. - - // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. - // The multifactorauth recipe will pick this value up next time the user is trying to login, and - // ask them to enter the OTP code. - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); - } - return response; - } - } - } - } - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + override: { + apis: (oI) => { + return { + ...oI, + consumeCodePOST: async function (input) { + let response = await oI.consumeCodePOST!(input); + if (response.status === "OK" && input.session !== undefined) { + // We do this only if a session exists, which means that it's not being called for first factor login. + + // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the OTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( + input.session.getUserId(), + MultiFactorAuth.FactorIds.OTP_EMAIL, + ); + } + return response; + }, + }; + }, + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, ) -from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from supertokens_python.recipe.session.interfaces import SessionContainer from supertokens_python.recipe.accountlinking.types import ( AccountInfoWithRecipeIdAndUserId, @@ -358,9 +369,8 @@ from supertokens_python.recipe.accountlinking.types import ( ) from supertokens_python.types import User from typing import Dict, Any, Optional, Union -from supertokens_python.recipe import passwordless from supertokens_python.recipe.passwordless.interfaces import ( - RecipeInterface, + RecipeInterface as PasswordlessRecipeInterface, ConsumeCodeOkResult, ) from supertokens_python.recipe.multifactorauth.asyncio import ( @@ -385,12 +395,12 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() -def override_functions(original_implementation: RecipeInterface): +def override_functions(original_implementation: PasswordlessRecipeInterface): original_consume_code = original_implementation.consume_code async def consume_code( @@ -434,46 +444,47 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), passwordless.init( contact_config=passwordless.ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(functions=override_functions), ), - multifactorauth.init( - first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], - ), accountlinking.init( should_do_automatic_account_linking=should_do_automatic_account_linking ), + multifactorauth.init( + first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], + ), ], ) ``` - - - + + - Initialize the multi-factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors a user has enabled and returns those. All that is needed is to mark `otp-email` as enabled for a user as soon as they have completed the OTP challenge successfully. This happens in the `consumeCodePOST` API override as shown above. Once the code is consumed successfully, mark the `otp-email` factor as enabled for the user, and the next time they login, they will be asked to complete the OTP challenge. - Notice that before calling `addToRequiredSecondaryFactorsForUser`, check if there is an input session or not. Only call `addToRequiredSecondaryFactorsForUser` function if there is a session which indicates that the user has finished some first factor already. - In both of the examples above, notice that the Passwordless recipe initializes in the `recipeList`. In this example, only email-based OTP is enabled, set the `contactMethod` to `EMAIL` and `flowType` to `USER_INPUT_CODE` (that is, OTP). If instead, you want to use phone SMS-based OTP, set the contact method to `PHONE`. If you want to give users both options, or for some users use email, and for others use phone, set `contactMethod` to `EMAIL_OR_PHONE`. -We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/docs/post-authentication/account-linking/automatic-account-linking). +We have also enabled the account linking feature since it's required for MFA to work. The above enables account linking for second factor only, but if you also want to enable it for first factor, see [this section](/post-authentication/account-linking/automatic-account-linking). -Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can link to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. +`shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload looks like this (for those that require OTP): ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -481,20 +492,19 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "otp-email": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "otp-email": 1702877999 + }, + "v": true + } } ``` -Indicating that the user has finished all required factors, and should access the app. +This indicates that the user has finished all required factors and should be allowed to access the app. -:::caution -If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again. +:::warning[If you are already using `Passwordless` or `ThirdPartyPasswordless` in your app as a first factor, you do not need to explicitly initialize the Passwordless recipe again.] ::: ### Frontend setup @@ -509,30 +519,30 @@ The second part, which is only applicable in case you want to allow users to ena - When the user navigates to their settings page, you can show them if OTP challenge is active or not. - If enabled, you can allow them to disable it, or vice versa. -To know if the user has enabled OTP, you can make an API your backend which calls the following function: +To know if the user has enabled OTP, you can create an API on your backend that calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isOTPEmailEnabledForUser(userId: string) { - let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) - return factors.includes(MultiFactorAuth.FactorIds.OTP_EMAIL) + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); + return factors.includes(MultiFactorAuth.FactorIds.OTP_EMAIL); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - + + ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -541,37 +551,36 @@ async def is_otp_email_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.OTP_EMAIL in factors ``` + + - - - -If the user wants to enable or disable otp-email for them, you can make an API on your backend which calls the following function: +If the user wants to enable or disable `otp-email`, you can create an API on your backend that calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL) + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL); } async function disableMFAForUser(userId: string) { - await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL) + await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.OTP_EMAIL); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - + + ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, @@ -586,12 +595,10 @@ async def enable_mfa_for_user(user_id: str) -> None: async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) ``` + + - - - -:::note -If instead you want to work with `otp-phone`, you can replace `otp-email` with `otp-phone` in the above snippets. Also make sure that the `contactMethod` configures to `PHONE` in the Passwordless recipe on the frontend (for pre-built UI) and backend. +:::note[If instead you want to work with `otp-phone`, you can replace `otp-email` with `otp-phone` in the above snippets. Also make sure that the `contactMethod` configures to `PHONE` in the Passwordless recipe on the frontend (for pre-built UI) and backend.] ::: @@ -604,110 +611,122 @@ However, if you want OTP to be active for a specific user, for a specific tenant #### Example 1: Only enable OTP for users that have an `admin` role - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" - -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" -import UserRoles from "supertokens-node/recipe/userroles" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - UserRoles.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-next-line - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) - // highlight-next-line - if (roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { - // we only want otp-email for admins - return [MultiFactorAuth.FactorIds.OTP_EMAIL] - } else { - // no MFA for non admin users. - return [] - } - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); + if ( + roles.roles.includes("admin") && + (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL) + ) { + // we only want otp-email for admins + return [MultiFactorAuth.FactorIds.OTP_EMAIL]; + } else { + // no MFA for non-admin users. + return []; + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking, passwordless +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, + userroles, +) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, @@ -743,7 +762,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -787,150 +806,163 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + userroles.init(), + emailpassword.init(), + thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), + accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), - accountlinking.init( - should_do_automatic_account_linking=should_do_automatic_account_linking - ), ], ) ``` + + - - - -- The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. - +- This override requires `otp-email` only when the user has the `admin` role and the tenant's `requiredSecondaryFactors` includes `otp-email`. #### Example 2: Ask for OTP only for users that have enabled OTP on their account - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import supertokens, { User, RecipeUserId, } from "supertokens-node"; +import supertokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" -import Passwordless from "supertokens-node/recipe/passwordless" -import Session from "supertokens-node/recipe/session" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import Session from "supertokens-node/recipe/session"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE", - override: { - apis: (oI) => { - return { - ...oI, - consumeCodePOST: async function (input) { - let response = await oI.consumeCodePOST!(input); - if (response.status === "OK" && input.session !== undefined) { - // We do this only if a session exists, which means that it's not being called for first factor login. - - // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. - // The multifactorauth recipe will pick this value up next time the user is trying to login, and - // ask them to enter the OTP code. - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.OTP_EMAIL); - } - return response; - } - } - } - } - }), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { - if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { - return [MultiFactorAuth.FactorIds.OTP_EMAIL] - } - } - // no otp-email required for input.user, with the input.tenant. - return [] - } - } + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + override: { + apis: (oI) => { + return { + ...oI, + consumeCodePOST: async function (input) { + let response = await oI.consumeCodePOST!(input); + if (response.status === "OK" && input.session !== undefined) { + // We do this only if a session exists, which means that it's not being called for first factor login. + + // OTP challenge completed successfully. We save that this user has enabled otp-email in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the OTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( + input.session.getUserId(), + MultiFactorAuth.FactorIds.OTP_EMAIL, + ); + } + return response; + }, + }; + }, + }, + }), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { + if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.OTP_EMAIL)) { + return [MultiFactorAuth.FactorIds.OTP_EMAIL]; } - } - // highlight-end - }) - ] -}) + } + // no otp-email required for input.user, with the input.tenant. + return []; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, accountlinking, passwordless +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) from supertokens_python.recipe.multifactorauth.types import ( FactorIds, OverrideConfig, @@ -973,7 +1005,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # new_account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -1048,29 +1080,30 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig(apis=passwordless_override), ), + accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), ), - accountlinking.init( - should_do_automatic_account_linking=should_do_automatic_account_linking - ), ], ) ``` + + - - - -An override for `getMFARequirementsForAuth` is available, which checks if otp-email is active for the user, and also considers the `tenantId` to decide if this user should go through the otp-email flow while logging into this tenant. The implementation of `shouldRequireOTPEmailForTenant` is entirely up to you. - +The `getMFARequirementsForAuth` override requires `otp-email` only when it is enabled for the user and the tenant's `requiredSecondaryFactors` includes `otp-email`. This lets the same user require OTP in selected tenants only. ### Frontend setup @@ -1084,7 +1117,6 @@ See the section on [protecting frontend and backend routes](../protect-routes). ## Email / SMS sending and design By default, the email template used for otp-email login is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login), and the default SMS template is [as shown here](https://github.com/SuperTokens/email-sms-templates?tab=readme-ov-file#otp-login-1). The method for sending them is via an email and SMS sending service that is available. -If you would like to learn more about this, or change the content of the email, or the method by which they send, checkout the email / SMS delivery section in the recipe docs: -- [Email delivery configuration](/docs/platform-configuration/email-delivery) -- [SMS delivery configuration](/docs/platform-configuration/sms-delivery) - +If you would like to learn more about this, change the content of the email, or change the method by which messages are sent, check out the email / SMS delivery section in the recipe docs: +- [Email delivery configuration](/platform-configuration/email-delivery) +- [SMS delivery configuration](/platform-configuration/sms-delivery) diff --git a/docs/additional-verification/mfa/embed-the-prebuilt-ui.mdx b/docs/additional-verification/mfa/embed-the-prebuilt-ui.mdx index 666b71d6bf..56e492684c 100644 --- a/docs/additional-verification/mfa/embed-the-prebuilt-ui.mdx +++ b/docs/additional-verification/mfa/embed-the-prebuilt-ui.mdx @@ -1,16 +1,10 @@ --- title: Embed the pre-built UI component -hide_title: true -sidebar_position: 8 description: Embed and customize a prebuilt MFA UI component. -page_type: guide -recipe: mfa -category: multi-factor-authentication +sidebar: + order: 90 --- - -# Embed the pre-built UI component - ## Overview ## Before you start @@ -26,204 +20,194 @@ The tutorial configures `TOTP` as a secondary factor, but the same set of steps The following example shows the scenario where you have a dedicated route, such as `/totp`, for rendering the TOTP Widget. Upon a successful login, the user will be automatically redirected to the return value of `getRedirectionURL` (defaulting to `/`). - - - - - + + +#### With React Router - - - +##### React Router v6 + + +:::warning[Not applicable to non-react apps. Please build your own custom UI instead.] +::: + + -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; -// highlight-end -// @ts-ignore +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - TOTP.init({ - totpMFAScreen: { - disableDefaultUI: true, - } - }), - MultiFactorAuth.init({ - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "GO_TO_FACTOR") { - if (context.factorId === "totp") { - return "/totp" - } - } - } - // highlight-end - }) - // ... - ], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + }, + }), + MultiFactorAuth.init({ + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp"; + } + } + }, + }), + // ... + ], }); function TOTPPage() { - const navigate = useNavigate(); - return ( -
-
- // highlight-next-line - -
-
- ); + const navigate = useNavigate(); + return ( +
+
+ +
+
+ ); } ``` +
+ - + +
- + + +##### React Router v5 + + -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; -// highlight-end -// @ts-ignore +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; import { useHistory } from "react-router-dom5"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - TOTP.init({ - totpMFAScreen: { - disableDefaultUI: true, - }, - }), - MultiFactorAuth.init({ - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "GO_TO_FACTOR") { - if (context.factorId === "totp") { - return "/totp" - } - } - } - // highlight-end - }) - // ... - ], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + }, + }), + MultiFactorAuth.init({ + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp"; + } + } + }, + }), + // ... + ], }); function TOTPPage() { - const history = useHistory(); - return ( -
-
- // highlight-next-line - -
-
- ); + const history = useHistory(); + return ( +
+
+ +
+
+ ); } ``` +
+
-
- + + +#### Without React Router + + -
- - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; -// highlight-end -// @ts-ignore +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - TOTP.init({ - totpMFAScreen: { - disableDefaultUI: true, - }, - }), - MultiFactorAuth.init({ - getRedirectionURL: async (context) => { - if (context.action === "GO_TO_FACTOR") { - if (context.factorId === "totp") { - return "/totp" - } - } - } - }) - // ... - ], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + TOTP.init({ + totpMFAScreen: { + disableDefaultUI: true, + }, + }), + MultiFactorAuth.init({ + getRedirectionURL: async (context) => { + if (context.action === "GO_TO_FACTOR") { + if (context.factorId === "totp") { + return "/totp"; + } + } + }, + }), + // ... + ], }); function TOTPPage() { - return ( -
-
- // highlight-next-line - -
-
- ) + return ( +
+
+ +
+
+ ); } ``` -
-
+ + + + In the above code snippet, we: 1. Disabled the default TOTP UI by setting `disableDefaultUI` to `true` inside the TOTP recipe config. 2. Overrode the `getRedirectionURL` function inside the MFA recipe config to redirect to `/totp` whenever we want to show the TOTP factor. Feel free to customize the redirection URLs as needed. - -
- - - -:::caution -Not applicable to non-react apps. Please build your own custom UI instead. -::: - - - -
+ + --- @@ -232,49 +216,100 @@ Not applicable to non-react apps. Please build your own custom UI instead. The following example shows the scenario where you embed the TOTP Widget in a popup, and upon successful login, you aim to close the popup. This is especially useful for step up auth. - - - - - - + + +#### With React Router - - - +##### React Router v6 + + +:::warning[Not applicable to non-react apps. Please build your own custom UI instead.] +::: + + + + ```tsx -import React, { useState, useEffect } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; -// highlight-end -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", - websiteDomain: "..." + websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), - MultiFactorAuth.init(/* ... */) + MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { - let sessionContext = Session.useSessionContext(); + const sessionContext = Session.useSessionContext(); const navigate = useNavigate(); + const location = useLocation(); + const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); - - const openModal = () => setIsModalOpen(true); - const closeModal = () => setIsModalOpen(false); + const [error, setError] = useState(); + + const openModal = () => { + const url = new URL(window.location.href); + const returnUrl = new URL(url); + returnUrl.searchParams.delete("stepUp"); + returnUrl.searchParams.delete("redirectToPath"); + returnUrl.searchParams.set("retryProtectedOperation", "true"); + + url.searchParams.delete("retryProtectedOperation"); + url.searchParams.set("stepUp", "true"); + url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); + window.history.replaceState(window.history.state, "", url); + retryStarted.current = false; + setError(undefined); + setIsModalOpen(true); + }; + + const cancelModal = () => { + const url = new URL(window.location.href); + url.searchParams.delete("stepUp"); + url.searchParams.delete("redirectToPath"); + url.searchParams.delete("retryProtectedOperation"); + window.history.replaceState(window.history.state, "", url); + setIsModalOpen(false); + }; + + useEffect(() => { + const params = new URLSearchParams(location.search); + if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { + return; + } + + retryStarted.current = true; + void (async () => { + try { + await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); + const response = await fetch("/api/sensitive-operation", { method: "POST" }); + if (!response.ok) { + throw new Error("The protected operation was rejected"); + } + + const url = new URL(window.location.href); + url.searchParams.delete("retryProtectedOperation"); + navigate(`${url.pathname}${url.search}${url.hash}`, { replace: true }); + setIsModalOpen(false); + } catch (error) { + setError(error instanceof Error ? error.message : "The protected operation failed"); + } + })(); + }, [location.search, navigate]); if (sessionContext.loading) { return null; @@ -286,58 +321,115 @@ function TOTPPopup() {

You are logged In!

UserId: {sessionContext.userId}

+
} - - {/* highlight-next-line */} + {error !== undefined &&

{error}

} + + +
); } ``` + + - + + - + + +##### React Router v5 + + + + ```tsx -import React, { useState, useEffect } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; -// highlight-end -import { useHistory } from "react-router-dom5"; +import { useHistory, useLocation } from "react-router-dom5"; SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", - websiteDomain: "..." + websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), - MultiFactorAuth.init(/* ... */) + MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { - let sessionContext = Session.useSessionContext(); + const sessionContext = Session.useSessionContext(); const history = useHistory(); + const location = useLocation(); + const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); - - const openModal = () => setIsModalOpen(true); - const closeModal = () => setIsModalOpen(false); + const [error, setError] = useState(); + + const openModal = () => { + const url = new URL(window.location.href); + const returnUrl = new URL(url); + returnUrl.searchParams.delete("stepUp"); + returnUrl.searchParams.delete("redirectToPath"); + returnUrl.searchParams.set("retryProtectedOperation", "true"); + + url.searchParams.delete("retryProtectedOperation"); + url.searchParams.set("stepUp", "true"); + url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); + window.history.replaceState(window.history.state, "", url); + retryStarted.current = false; + setError(undefined); + setIsModalOpen(true); + }; + + const cancelModal = () => { + const url = new URL(window.location.href); + url.searchParams.delete("stepUp"); + url.searchParams.delete("redirectToPath"); + url.searchParams.delete("retryProtectedOperation"); + window.history.replaceState(window.history.state, "", url); + setIsModalOpen(false); + }; + + useEffect(() => { + const params = new URLSearchParams(location.search); + if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { + return; + } + + retryStarted.current = true; + void (async () => { + try { + await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); + const response = await fetch("/api/sensitive-operation", { method: "POST" }); + if (!response.ok) { + throw new Error("The protected operation was rejected"); + } + + const url = new URL(window.location.href); + url.searchParams.delete("retryProtectedOperation"); + history.replace(`${url.pathname}${url.search}${url.hash}`); + setIsModalOpen(false); + } catch (error) { + setError(error instanceof Error ? error.message : "The protected operation failed"); + } + })(); + }, [history, location.search]); if (sessionContext.loading) { return null; @@ -349,59 +441,109 @@ function TOTPPopup() {

You are logged In!

UserId: {sessionContext.userId}

+
} - - {/* highlight-next-line */} + {error !== undefined &&

{error}

} + + + ); } ``` +
+
-
- - - - - + + +#### Without React Router + + + + ```tsx -import React, { useState, useEffect } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Modal from "react-modal"; import SuperTokens from "supertokens-auth-react"; import TOTP from "supertokens-auth-react/recipe/totp"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -// highlight-start -import { MFATOTP } from 'supertokens-auth-react/recipe/totp/prebuiltui'; +import { MFATOTP } from "supertokens-auth-react/recipe/totp/prebuiltui"; import Session from "supertokens-auth-react/recipe/session"; -// highlight-end SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", - websiteDomain: "..." + websiteDomain: "...", }, recipeList: [ TOTP.init(/* ... */), - MultiFactorAuth.init(/* ... */) + MultiFactorAuth.init(/* ... */), // ... ], }); function TOTPPopup() { - let sessionContext = Session.useSessionContext(); + const sessionContext = Session.useSessionContext(); + const retryStarted = useRef(false); const [isModalOpen, setIsModalOpen] = useState(false); - - const openModal = () => setIsModalOpen(true); - const closeModal = () => setIsModalOpen(false); + const [error, setError] = useState(); + + const openModal = () => { + const url = new URL(window.location.href); + const returnUrl = new URL(url); + returnUrl.searchParams.delete("stepUp"); + returnUrl.searchParams.delete("redirectToPath"); + returnUrl.searchParams.set("retryProtectedOperation", "true"); + + url.searchParams.delete("retryProtectedOperation"); + url.searchParams.set("stepUp", "true"); + url.searchParams.set("redirectToPath", `${returnUrl.pathname}${returnUrl.search}${returnUrl.hash}`); + window.history.replaceState(window.history.state, "", url); + retryStarted.current = false; + setError(undefined); + setIsModalOpen(true); + }; + + const cancelModal = () => { + const url = new URL(window.location.href); + url.searchParams.delete("stepUp"); + url.searchParams.delete("redirectToPath"); + url.searchParams.delete("retryProtectedOperation"); + window.history.replaceState(window.history.state, "", url); + setIsModalOpen(false); + }; + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get("retryProtectedOperation") !== "true" || retryStarted.current) { + return; + } + + retryStarted.current = true; + void (async () => { + try { + await MultiFactorAuth.resyncSessionAndFetchMFAInfo(); + const response = await fetch("/api/sensitive-operation", { method: "POST" }); + if (!response.ok) { + throw new Error("The protected operation was rejected"); + } + + const url = new URL(window.location.href); + url.searchParams.delete("retryProtectedOperation"); + window.history.replaceState(window.history.state, "", url); + setIsModalOpen(false); + } catch (error) { + setError(error instanceof Error ? error.message : "The protected operation failed"); + } + })(); + }, []); if (sessionContext.loading) { return null; @@ -413,33 +555,22 @@ function TOTPPopup() {

You are logged In!

UserId: {sessionContext.userId}

+
} - - {/* highlight-next-line */} + {error !== undefined &&

{error}

} + + + ); } ``` -
- - - - - - - -:::caution -Not applicable to non-react apps. Please build your own custom UI instead. -::: - - + + - +The `retryProtectedOperation` return marker is not proof that step-up authentication succeeded. After the factor flow returns, call `MultiFactorAuth.resyncSessionAndFetchMFAInfo()` to synchronize the session and retry the server-protected operation. The MFA freshness validator on the backend is authoritative and must reject the operation if the required factor is missing or too old. The **Cancel** button only cancels the popup; it must not retry or authorize the operation. diff --git a/docs/additional-verification/mfa/hooks-and-overrides.mdx b/docs/additional-verification/mfa/hooks-and-overrides.mdx index f761315c1c..c521a9732c 100644 --- a/docs/additional-verification/mfa/hooks-and-overrides.mdx +++ b/docs/additional-verification/mfa/hooks-and-overrides.mdx @@ -1,154 +1,139 @@ --- title: Hooks and overrides -hide_title: true -sidebar_position: 9 -description: >- - Configure your application for multi-factor authentication with account - linking and tenant-specific configurations. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Configure your application for multi-factor authentication with account linking and tenant-specific configurations. +sidebar: + order: 110 --- - -# Hooks and overrides - **SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `mfa` recipe to your needs. -Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. +Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Frontend event hooks The pre-built UI emits a few events that you can listen to on the frontend. As an example, you can use these for analytics: - - - - + + ```tsx -import SuperTokens from "supertokens-auth-react" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import SuperTokens from "supertokens-auth-react"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; import TOTP from "supertokens-auth-react/recipe/totp"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - onHandleEvent: (context) => { - if (context.action === "PASSWORDLESS_CODE_SENT") { - // this event is fired when the user has successfully sent out an OTP email / SMS - } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { - // This event is fired when the user's OTP has expired, or - // they have reached the max limit of number of failed OTP attempts. - } else if (context.action === "SUCCESS" && !context.createdNewSession) { - // this event is fired when successfully completing the OTP email / SMS challenge - // and if it's not used in first factor (cause we do !context.createdNewSession) - } - } - }), - TOTP.init({ - onHandleEvent: (context) => { - if (context.action === "TOTP_DEVICE_CREATED") { - // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. - } else if (context.action === "TOTP_DEVICE_VERIFIED") { - // this event is fired during factor setup, when the user has successfully verified the TOTP device - } else if (context.action === "TOTP_CODE_VERIFIED") { - // this event is fired when the user has successfully verified the TOTP code - // marking the TOTP factor as completed - } - } - }), - MultiFactorAuth.init({ - firstFactors: [/*...*/], - onHandleEvent: (context) => { - if (context.action === "FACTOR_CHOOSEN") { - let chosenFactorId = context.factorId; - // this event is fired when the user is shown the screen for - // picking one factor out of a choice of multiple factors - } - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + onHandleEvent: (context) => { + if (context.action === "PASSWORDLESS_CODE_SENT") { + // this event is fired when the user has successfully sent out an OTP email / SMS + } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { + // This event is fired when the user's OTP has expired, or + // they have reached the max limit of number of failed OTP attempts. + } else if (context.action === "SUCCESS" && !context.createdNewSession) { + // this event is fired when successfully completing the OTP email / SMS challenge + // and if it's not used in first factor (cause we do !context.createdNewSession) + } + }, + }), + TOTP.init({ + onHandleEvent: (context) => { + if (context.action === "TOTP_DEVICE_CREATED") { + // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. + } else if (context.action === "TOTP_DEVICE_VERIFIED") { + // this event is fired during factor setup, when the user has successfully verified the TOTP device + } else if (context.action === "TOTP_CODE_VERIFIED") { + // this event is fired when the user has successfully verified the TOTP code + // marking the TOTP factor as completed + } + }, + }), + MultiFactorAuth.init({ + firstFactors: [ + /*...*/ + ], + onHandleEvent: (context) => { + if (context.action === "FACTOR_CHOOSEN") { + let chosenFactorId = context.factorId; + // this event is fired when the user is shown the screen for + // picking one factor out of a choice of multiple factors + } + }, + }), + ], +}); ``` - - - - - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUITOTP from "supertokens-auth-react-script/recipe/totp"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - onHandleEvent: (context) => { - if (context.action === "PASSWORDLESS_CODE_SENT") { - // this event is fired when the user has successfully sent out an OTP email / SMS - } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { - // This event is fired when the user's OTP has expired, or - // they have reached the max limit of number of failed OTP attempts. - } else if (context.action === "SUCCESS" && !context.createdNewSession) { - // this event is fired when successfully completing the OTP email / SMS challenge - // and if it's not used in first factor (cause we do !context.createdNewSession) - } - } - }), - supertokensUITOTP.init({ - onHandleEvent: (context) => { - if (context.action === "TOTP_DEVICE_CREATED") { - // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. - } else if (context.action === "TOTP_DEVICE_VERIFIED") { - // this event is fired during factor setup, when the user has successfully verified the TOTP device - } else if (context.action === "TOTP_CODE_VERIFIED") { - // this event is fired when the user has successfully verified the TOTP code - // marking the TOTP factor as completed - } - } - }), - supertokensUIMultiFactorAuth.init({ - firstFactors: [/*...*/], - onHandleEvent: (context) => { - if (context.action === "FACTOR_CHOOSEN") { - let chosenFactorId = context.factorId; - // this event is fired when the user is shown the screen for - // picking one factor out of a choice of multiple factors - } - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", + onHandleEvent: (context) => { + if (context.action === "PASSWORDLESS_CODE_SENT") { + // this event is fired when the user has successfully sent out an OTP email / SMS + } else if (context.action === "PASSWORDLESS_RESTART_FLOW") { + // This event is fired when the user's OTP has expired, or + // they have reached the max limit of number of failed OTP attempts. + } else if (context.action === "SUCCESS" && !context.createdNewSession) { + // this event is fired when successfully completing the OTP email / SMS challenge + // and if it's not used in first factor (cause we do !context.createdNewSession) + } + }, + }), + supertokensUITOTP.init({ + onHandleEvent: (context) => { + if (context.action === "TOTP_DEVICE_CREATED") { + // this event is fired during factor setup, when the user has successfully created the TOTP device. They still have to verify it by entering the TOTP. + } else if (context.action === "TOTP_DEVICE_VERIFIED") { + // this event is fired during factor setup, when the user has successfully verified the TOTP device + } else if (context.action === "TOTP_CODE_VERIFIED") { + // this event is fired when the user has successfully verified the TOTP code + // marking the TOTP factor as completed + } + }, + }), + supertokensUIMultiFactorAuth.init({ + firstFactors: [ + /*...*/ + ], + onHandleEvent: (context) => { + if (context.action === "FACTOR_CHOOSEN") { + let chosenFactorId = context.factorId; + // this event is fired when the user is shown the screen for + // picking one factor out of a choice of multiple factors + } + }, + }), + ], +}); ``` - - - - + + ## Backend overrides It's a common use case to want to override the default behavior of SuperTokens after a user signs up or signs in. For example, you may want to change your database state whenever someone signs up. You can do this by overriding the sign up / sign in recipe functions in the backend SDK: -- [Passwordless recipe](/docs/authentication/passwordless/hooks-and-overrides) -- [EmailPassword recipe](/docs/authentication/email-password/hooks-and-overrides) -- [ThirdParty recipe](/docs/authentication/social/hooks-and-overrides) +- [Passwordless recipe](/authentication/passwordless/hooks-and-overrides) +- [EmailPassword recipe](/authentication/email-password/hooks-and-overrides) +- [ThirdParty recipe](/authentication/social/hooks-and-overrides) Since the sign up / sign in APIs share functionality for first factor and second factor login, your override applies to both first and second factor login. If you want to have different behavior for first and second factor login, you can use the `input` argument to the function to determine if the user is doing first or second factor login. diff --git a/docs/additional-verification/mfa/important-concepts.mdx b/docs/additional-verification/mfa/important-concepts.mdx index 3b0a5486fe..d62871ad72 100644 --- a/docs/additional-verification/mfa/important-concepts.mdx +++ b/docs/additional-verification/mfa/important-concepts.mdx @@ -1,20 +1,10 @@ --- -id: important-concepts title: Important concepts -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 -description: >- - Understand SuperTokens auth factor IDs, completion status, and multi-factor - authentication setup. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Understand SuperTokens auth factor IDs, completion status, and multi-factor authentication setup. +sidebar: + order: 20 --- - -# Important concepts - ## Overview If you are new to Multi-factor authentication, MFA, this page provides a quick summary of how it works and the main terminology used in it. @@ -22,19 +12,15 @@ If you are new to Multi-factor authentication, MFA, this page provides a quick s MFA enhances security by requiring users to authenticate through: - -## **Initial login** - - The user enters their primary credentials, typically a username and password (first factor). - -## **Authentication challenge** - + +The user enters their primary credentials, typically a username and password (first factor). + + Upon successful entry of the primary credentials, the user receives an authentication challenge, prompting them to provide a secondary factor, such as an OTP or biometric verification. - -## **Access granted** - + + Access to the account or service is only granted when both the primary and secondary factors are successfully verified. - + This layered approach reduces the risk of unauthorized access, as an attacker would need to compromise multiple authentication methods to gain access. @@ -67,7 +53,7 @@ Each auth challenge has a factor ID in SuperTokens: | TOTP | `totp` | | WebAuthn/Passkeys | `webauthn` | -These factor IDs get used to configure the MFA requirements for users (except the `acccess-denied` one). +These factor IDs get used to configure the MFA requirements for users (except the `access-denied` one). They are also used to indicate which authentication challenges have completed in the current session. #### Factor completion status @@ -77,18 +63,18 @@ In the payload you can find the following claim structure: ```json { - "st-mfa": { - // c stands for completed, and - // Shows that only the emailpassword factor has been completed - "c": { - // the timestamp when the factor was completed - "emailpassword": 1702877939, - }, - // v stands for value - // In this example, the false value indicates that the MFA flow is not completed - // Once the second factor is finalized v will change to true - "v": false - } + "st-mfa": { + // c stands for completed, and + // Shows that only the emailpassword factor has been completed + "c": { + // the timestamp when the factor was completed + "emailpassword": 1702877939 + }, + // v stands for value + // In this example, the false value indicates that the MFA flow is not completed + // Once the second factor is finalized v will change to true + "v": false + } } ``` @@ -110,8 +96,6 @@ The first factors are those allowed to create a new session, whereas any other f In case the user calls an additional factor's API without a session, the API responds with a `401` error. - - ### Account linking Account linking refers to the process of connecting multiple accounts or identities across platforms or services. @@ -125,7 +109,7 @@ During account linking, the individual login methods create their own "recipe us Theoretically, one can link any recipe user to another (there is no need for them to have the same email or phone number). However, for first factor automatic linking, only link login methods if they have been verified and have the same email. -From an MFA point of view, whenever the user sets up a new passwordless factor (otp-email or otp-sms), this creates a new recipe user for the passwordless recipe. It then auto-links it to the existing session's recipe user. +From an MFA point of view, whenever the user sets up a new passwordless factor (`otp-email` or `otp-phone`), this creates a new recipe user for the passwordless recipe. It then auto-links it to the existing session's recipe user. Therefore, it is necessary to enable account linking for MFA to work. In the MFA guide, first factor account linking is not enabled, but you can enable that by following the automatic account linking guide in other parts of the docs. @@ -133,11 +117,10 @@ In the MFA guide, first factor account linking is not enabled, but you can enabl ## See also - - - - - - - - + + + + + + + diff --git a/docs/additional-verification/mfa/initial-setup.mdx b/docs/additional-verification/mfa/initial-setup.mdx index 430571ad5d..c75cd1847a 100644 --- a/docs/additional-verification/mfa/initial-setup.mdx +++ b/docs/additional-verification/mfa/initial-setup.mdx @@ -1,19 +1,16 @@ --- title: Initial setup -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 3 -description: >- - Configure your application for multi-factor authentication with account - linking and tenant-specific configurations. -page_type: tutorial -recipe: mfa -category: multi-factor-authentication +description: Configure your application for multi-factor authentication with account linking and tenant-specific configurations. +sidebar: + order: 30 --- - -# Initial setup - + +Design and implement SuperTokens MFA in this application. Inspect the existing recipes, tenant model, and deployment type first. Ask which first factors, second factors, and enforcement policy are required if they cannot be inferred. Check managed-service and SDK support, configure account linking, tenant-specific factors, email verification dependencies, frontend and backend recipes, and protected routes. Do not commit secrets. Validate first-factor login, second-factor enrollment, challenge completion, recovery, and route protection with the relevant tests and build. + ## Overview @@ -25,14 +22,14 @@ The following guide shows you how to implement a basic setup while also covering These instructions assume that you already have some knowledge of MFA. -If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). +If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). If you plan to use the `otp-email` factor as a form of email verification, you also need to initialize the `emailverification` recipe in `REQUIRED` mode on the backend. This configuration ensures that the email verification process passes only if the originally provided email has been verified. ## Steps - + ### 1. Set up the backend @@ -40,67 +37,71 @@ This configuration ensures that the email verification process passes only if th MFA requires account linking to be active. You can enable it in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import SuperTokens, { User, RecipeUserId, } from "supertokens-node"; +import SuperTokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import AccountLinking from "supertokens-node/recipe/accountlinking" +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - // highlight-start - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: UserContext) => { - if (session === undefined) { - // we do not want to do first factor account linking by default. To enable that, - // please see the automatic account linking docs in the recipe docs for your first factor. - return { - shouldAutomaticallyLink: false - }; - } - if (user === undefined || session.getUserId() === user.id) { - // if it comes here, it means that a session exists, and we are trying to link the - // newAccountInfo to the session user, which means it's an MFA flow, so we enable - // linking here. - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: false - } - } - return { - shouldAutomaticallyLink: false - }; - } - }), - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: UserContext, + ) => { + if (session === undefined) { + // we do not want to do first factor account linking by default. To enable that, + // please see the automatic account linking docs in the recipe docs for your first factor. + return { + shouldAutomaticallyLink: false, + }; + } + if (user === undefined || session.getUserId() === user.id) { + // if it comes here, it means that a session exists, and we are trying to link the + // newAccountInfo to the session user, which means it's an MFA flow, so we enable + // linking here. + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking from supertokens_python.types import User @@ -125,7 +126,7 @@ async def should_do_automatic_account_linking( # If it comes here, it means that a session exists, and we are trying to link the # account_info to the session user, which means it's an MFA flow, so we enable # linking here. - return ShouldAutomaticallyLink(should_require_verification=False) + return ShouldAutomaticallyLink(should_require_verification=True) return ShouldNotAutomaticallyLink() @@ -139,89 +140,88 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework='...', # type: ignore + framework='...', recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - - - + + - The above snippet enables auto account linking only during the second factor and not for the first factor login. This means that if a user has an email password account, and then they login via Google (with the same email), those two accounts are not linked. However, if the second factor for logging in is email or phone OTP, then that passwordless account links to the first factor login method of that session. -- Notice that `shouldRequireVerification: false` configures account linking. It means that the second factor can connect to the first factor even though the first factor is not verified. If you want to do email verification of the first factor before setting up the second factor (for example if the first factor is email password, and the second is phone OTP), then you can set this boolean to `true`, and also init the email verification recipe on the frontend and backend in `REQUIRED` mode. -- If you also want to enable first factor automatic account linking, see [this link](/docs/post-authentication/account-linking/automatic-account-linking). +- `shouldRequireVerification: true` prevents an unverified login method from being linked. Passwordless OTP completion verifies the email address or phone number before the SDK attempts second-factor linking, so this does not block the OTP flow. Keep the callback session-bound as shown; do not return automatic linking for first-factor requests without a session. +- If you also want to enable first factor automatic account linking, see [this link](/post-authentication/account-linking/automatic-account-linking). -:::important -Account linking is a paid feature, and you need to generate a license key to enable it. Enabling the MFA feature also enables account linking automatically, meaning you don't need to check the account linking feature. +:::note[Account linking is also a paid feature. Enabling MFA enables the account-linking capability required by this flow, so you do not need to enable both features separately.] ::: #### 1.2 Configure the first factors - + -We start by initializing the MFA recipe on the backend and specifying the list of first factors using their [factor IDs](/docs/additional-verification/mfa/important-concepts#factors). You still have to initialize all the auth recipes in the `recipeList`, and configure them based on your needs. +We start by initializing the MFA recipe on the backend and specifying the list of first factors using their [factor IDs](/additional-verification/mfa/important-concepts#factors). You still have to initialize all the auth recipes in the `recipeList`, and configure them based on your needs. For example, the code below initializes `thirdparty`, `emailpassword` and `passwordless` recipes and sets the `firstFactor` array to be `["emailpassword", "thirdparty"]`. This means that email password and social login appear to the user as the first factor (using the `thirdparty` + `emailpassword` recipe), and `passwordless` serves as the second factor. - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - // highlight-start - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import emailpassword, multifactorauth, thirdparty, passwordless +from supertokens_python.recipe import ( + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -234,70 +234,67 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework='...', # type: ignore + framework='...', recipe_list=[ + session.init(), emailpassword.init(), thirdparty.init(), - multifactorauth.init(), passwordless.init(contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE"), multifactorauth.init(first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY]) ], ) ``` - - - + + Other combinations of first factors exists. For example, if you want passwordless as the first factor, then you would init the passwordless recipe and add `"passwordless"` in the `firstFactors` array. - + - + For a multi-tenancy setup, where each tenant can have a different set of first factors, you can leave the `firstFactors` array as `undefined` in the `MultiFactorAuth.init`. Configure the `firstFactors` on a per-tenant basis when creating or updating a tenant as shown below: - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + +Email Password enabled + +In the above setting, Email Password is active in the **Login Methods** section. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. - +By default, no login methods activate for a tenant. + + + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } } ``` + + -`firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. - -:::important -- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default. -- To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. -::: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -316,10 +313,8 @@ async def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -338,15 +333,11 @@ def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - - - - - -```bash showAppTypeSelect + + + + +```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ @@ -355,116 +346,115 @@ curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/ "firstFactors": ["emailpassword"] }' ``` + + + + + + + `firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. -:::important -- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default. +:::note[- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default.] + - To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. ::: + + +`firstFactors` includes only `"emailpassword"`. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. - - - - -Email Password enabled - -In the above setting, Email Password is active in the **Login Methods** section. This means that users who login to this tenant can only use email password as the first factor. Later on, the configuration for passwordless as a second factor for this tenant appears. - -By default, no login methods activate for a tenant. - - +:::note[- If you do not configure `firstFactors` array on a tenant configuration, then no factors activate for that tenant by default.] - +- To remove the `firstFactors` configuration for a tenant, you can pass a `null` value for the `firstFactors` key in the tenant configuration. For that tenant, this makes SuperTokens default to the `firstFactors` array in the `MultiFactorAuth.init` from the backend `init` configuration. +::: + + - + #### 1.3 Configure the second factor This section explains how to configure SuperTokens such that a second factor is necessary for all users during sign up and during sign in. TOTP serves as an example for the second factor. - + The following code snippet accomplishes this: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts -import SuperTokens, { User, RecipeUserId, } from "supertokens-node"; +import SuperTokens, { User, RecipeUserId } from "supertokens-node"; import { UserContext } from "supertokens-node/types"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import AccountLinking from "supertokens-node/recipe/accountlinking" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; - SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Passwordless.init({ - contactMethod: "EMAIL", - flowType: "USER_INPUT_CODE" - }), - totp.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - return [MultiFactorAuth.FactorIds.TOTP] - } - } - } - } - // highlight-end - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + totp.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [MultiFactorAuth.FactorIds.TOTP]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import ( emailpassword, multifactorauth, - thirdparty, passwordless, + session, + thirdparty, + totp, ) from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig @@ -502,14 +492,15 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), emailpassword.init(), thirdparty.init(), - multifactorauth.init(), passwordless.init( contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" ), + totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), @@ -517,9 +508,8 @@ init( ], ) ``` - - - + + In the above snippet, you configure email password and social login as the first factor, followed by TOTP as the second factor. @@ -531,67 +521,67 @@ In the above example, `"totp"` returns as a required factor for all users. Howev The output of this function can be more complex than a `string[]`. You can also return an object which tells SuperTokens that any one of the factors must satisfy: + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + -## Require one of multiple factors - - - - + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - // highlight-start - return [{ - oneOf: [ - MultiFactorAuth.FactorIds.TOTP, - MultiFactorAuth.FactorIds.OTP_EMAIL - ] - }] - // highlight-end - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [ + { + oneOf: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL], + }, + ]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth +from supertokens_python.recipe import ( + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, + totp, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List @@ -610,7 +600,6 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-next-line return [FactorIds.TOTP, FactorIds.OTP_EMAIL] original_implementation.get_mfa_requirements_for_auth = ( @@ -628,8 +617,15 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), + totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), @@ -637,70 +633,70 @@ init( ], ) ``` + + + + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + - - - -## Require multiple factors in any order - - - - + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - // highlight-start - return [{ - allOfInAnyOrder: [ - MultiFactorAuth.FactorIds.TOTP, - MultiFactorAuth.FactorIds.OTP_EMAIL - ] - }] - // highlight-end - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [ + { + allOfInAnyOrder: [MultiFactorAuth.FactorIds.TOTP, MultiFactorAuth.FactorIds.OTP_EMAIL], + }, + ]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth +from supertokens_python.recipe import ( + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, + totp, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List @@ -719,7 +715,6 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-next-line return [{"allOfInAnyOrder": [FactorIds.TOTP, FactorIds.OTP_EMAIL]}] original_implementation.get_mfa_requirements_for_auth = ( @@ -737,8 +732,15 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), + totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), @@ -746,73 +748,76 @@ init( ], ) ``` + + + + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + - - - -## Require multiple factors in a specific order - - - - + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - // highlight-start - let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload(input.accessTokenPayload) - if (MultiFactorAuth.FactorIds.TOTP in currentCompletedFactors.c) { - // this means the totp factor is completed - return [MultiFactorAuth.FactorIds.OTP_EMAIL] - } else { - // this means we have not finished totp yet, and we want - // to do that right after first factor login - return [MultiFactorAuth.FactorIds.TOTP] - } - // highlight-end - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let currentCompletedFactors = MultiFactorAuth.MultiFactorAuthClaim.getValueFromPayload( + input.accessTokenPayload, + ); + if (MultiFactorAuth.FactorIds.TOTP in currentCompletedFactors.c) { + // this means the totp factor is completed + return [MultiFactorAuth.FactorIds.OTP_EMAIL]; + } else { + // this means we have not finished totp yet, and we want + // to do that right after first factor login + return [MultiFactorAuth.FactorIds.TOTP]; + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth +from supertokens_python.recipe import ( + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, + totp, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List @@ -834,7 +839,6 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-start current_completed_factors = MultiFactorAuthClaim.get_value_from_payload( access_token_payload ) @@ -845,7 +849,6 @@ def override_functions(original_implementation: RecipeInterface): # this means we have not finished totp yet, and we want # to do that right after first factor login return [FactorIds.TOTP] - # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth @@ -862,8 +865,15 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), + totp.init(), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), @@ -871,65 +881,64 @@ init( ], ) ``` - - - - + + +
-:::note no-title -You can return an empty array from `getMFARequirementsForAuth` if you don't want any further MFA done for the current user. +:::note[You can return an empty array from `getMFARequirementsForAuth` if you don't want any further MFA done for the current user.] ::: -
+ - + For a multi tenant setup, you can configure a list of secondary factors when creating / modifying a tenant as shown below: - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + +OTP - Email enabled + +As shown above, you turn on `OTP - Email` in the **Secondary Factors** section which means that all users who log into that tenant must complete `otp-email` as a second factor. + +You can also turn off all factors to have no secondary factors required for the tenant. - +If you turn on more than one factor, it means that the user must complete any one of factors that are active. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: + + + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], - requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD], + requiredSecondaryFactors: [MultiFactorAuth.FactorIds.OTP_EMAIL], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } } ``` + + -In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. - -To remove the `requiredSecondaryFactors` configuration for a tenant, you can pass a `null` value for the `requiredSecondaryFactors` key in the tenant configuration. - -If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -951,10 +960,8 @@ async def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -976,14 +983,10 @@ def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - - - - - + + + + ```bash curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ --header 'api-key: YOUR_API_KEY' \ @@ -994,85 +997,88 @@ curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/ "requiredSecondaryFactors": ["otp-email"] }' ``` + + + + + + + In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. -To remove the `requiredSecondaryFactors` configuration for a tenant, you can turn off all the toggles. +To remove the `requiredSecondaryFactors` configuration for a tenant, you can pass a `null` value for the `requiredSecondaryFactors` key in the tenant configuration. If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: + + +In the above code, you add a property called `requiredSecondaryFactors` for a tenant whose value is a `string[]`. You add `otp-email` as a factor ID above which means that all users who log into that tenant must complete `otp-email` as a second factor. - - - - -OTP - Email enabled - -As shown above, you turn on `OTP - Email` in the **Secondary Factors** section which means that all users who log into that tenant must complete `otp-email` as a second factor. - -You can also turn off all factors to have no secondary factors required for the tenant. - -If you turn on more than one factor, it means that the user must complete any one of factors that are active. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: - - - +To remove the `requiredSecondaryFactors` configuration for a tenant, you can turn off all the toggles. - +If you add more than one item in this array, it means that the user must complete any one of factors mentioned in the array. If you want to have a different behavior for the tenant, you can achieve that by overriding the `getMFARequirementsForAuth` function as shown below: + + - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - // highlight-start - return [{ - allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant - }] - // highlight-end - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [ + { + allOfInAnyOrder: await input.requiredSecondaryFactorsForTenant, + }, + ]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth +from supertokens_python.recipe import ( + emailpassword, + multifactorauth, + passwordless, + session, + thirdparty, +) +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface from typing import Dict, Any, Callable, Awaitable, List @@ -1091,9 +1097,7 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-start return [{"allOfInAnyOrder": await required_secondary_factors_for_tenant()}] - # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth @@ -1110,8 +1114,14 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ + session.init(), + emailpassword.init(), + thirdparty.init(), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), flow_type="USER_INPUT_CODE" + ), multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], override=OverrideConfig(functions=override_functions), @@ -1119,46 +1129,47 @@ init( ], ) ``` + + - - + + Notice that the input to the function contains the `requiredSecondaryFactorsForTenant` array. This would be the same list that you passed to the tenant configuration when creating / modifying the tenant as shown in the previous steps. - + -#### 1.4 Remove the second factor requirement {{optional}} +#### 1.4 Remove the second factor requirement (optional) Instead of configuring a factor for all users in your app, or for all users within a tenant, you may want to implement a flow in which users do MFA only if they have enabled it for themselves. Here, users may also want to choose what factors they would like to enable for themselves. This flow allows users to configure their MFA preferences in the settings page in your app's frontend. A pre-built UI for this is not yet provided, but in this section, we explain the setup on the backend. -You want to start by creating an API that does [session verification](/docs/additional-verification/session-verification/protect-api-routes), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API: +You want to start by creating an API that does [session verification](/additional-verification/session-verification/protect-api-routes), and then enable the desired factor for the user. For example, if the user wants to enable TOTP, then you would call the following function in your API: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1168,10 +1179,8 @@ async def enable_mfa_for_user(user_id: str): user_id, FactorIds.TOTP ) ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import add_to_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1181,70 +1190,65 @@ def enable_mfa_for_user(user_id: str): user_id, FactorIds.TOTP ) ``` - - - - - - + + + + The effect of the above function call is that in the default implementation of `getMFARequirementsForAuth`, the factors specifically enabled for the input user are considered. By default, if you add multiple factors for a user ID, then it would require them to complete any one of those secondary factors during login. If you want to change the default behavior from "any one of" to something else (like "all of"), you can do this by overriding the `getMFARequirementsForAuth` function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // highlight-start - getMFARequirementsForAuth: async function (input) { - return [{ - allOfInAnyOrder: await input.requiredSecondaryFactorsForUser - }] - } - // highlight-end - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [ + { + allOfInAnyOrder: await input.requiredSecondaryFactorsForUser, + }, + ]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth from supertokens_python.recipe.multifactorauth.types import FactorIds, OverrideConfig @@ -1265,9 +1269,7 @@ def override_functions(original_implementation: RecipeInterface): required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], user_context: Dict[str, Any], ) -> MFARequirementList: - # highlight-start return [{"allOfInAnyOrder": await required_secondary_factors_for_user()}] - # highlight-end original_implementation.get_mfa_requirements_for_auth = ( get_mfa_requirements_for_auth @@ -1284,7 +1286,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], @@ -1293,49 +1295,47 @@ init( ], ) ``` - - - + + Once you call the `addToRequiredSecondaryFactorsForUser` function for a user, SuperTokens stores this preference in the user metadata JSON of the user. For example, if you add `"totp"` as a required secondary factor for a user, this preference is stored in the metadata JSON as: ```json { - "_supertokens": { - "requiredSecondaryFactors": ["totp"] - } + "_supertokens": { + "requiredSecondaryFactors": ["totp"] + } } ``` -You can view this JSON on the [user details page of the user management dashboard](/docs/post-authentication/dashboard/user-management) and modify it manually if you like. +You can view this JSON on the [user details page of the user management dashboard](/post-authentication/dashboard/user-management) and modify it manually if you like. To know the factors that a user has enabled, you can use the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { - let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) - return factors.includes(MultiFactorAuth.FactorIds.TOTP) + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); + return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1346,10 +1346,8 @@ async def is_totp_enabled_for_user(user_id: str): ) return FactorIds.TOTP in factors ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1360,42 +1358,39 @@ def is_totp_enabled_for_user(user_id: str): ) return FactorIds.TOTP in factors ``` - - - - - - + + + + Using the above function, you can build your settings page on the frontend which displays the existing enabled factors for the user. Allow users to enable or disable factors as they like. Once you have enabled a factor for a user, you take them to that factor setup screen if they have not previously already setup the factor. To know if a factor is setup, you can call the following function (on the backend): - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpSetupForUser(userId: string) { - let factors = await MultiFactorAuth.getFactorsSetupForUser(userId) - return factors.includes(MultiFactorAuth.FactorIds.TOTP) + let factors = await MultiFactorAuth.getFactorsSetupForUser(userId); + return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1406,10 +1401,8 @@ async def is_totp_enabled_for_user(user_id: str): ) return FactorIds.TOTP in factors ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import get_factors_setup_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1420,21 +1413,18 @@ def is_totp_enabled_for_user(user_id: str): ) return FactorIds.TOTP in factors ``` + + + + - - - - - - -Or you can call the [`MFAInfo` endpoint](/docs/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) from the frontend which returns information indicating which factors have already been setup for the user and which not. +Or you can call the [`MFAInfo` endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) from the frontend which returns information indicating which factors have already been setup for the user and which not. A factor is considered setup if the user has gone through that factor's flow at least once. For example, if the user has created and verified a TOTP device, only then does the `getFactorsSetupForUser` function return `totp` as part of the array. Likewise, if the user has completed `otp-email` or `link-email` once, only then do these factors become a part of the returned array. Let's take two examples: - The first time the user enables TOTP, then the result of `getFactorsSetupForUser` does not contain `"totp"`. You should redirect the user to the TOTP setup screen. Once they add and verify a device, then `getFactorsSetupForUser` returns `["totp"]` even if they later disable TOTP from the settings page and re-enable it. - Let's say that the first factor for a user is `emailpassword`, and the second factor is `otp-email`. Once they sign up, SuperTokens already knows the email for the user, when they are doing the `otp-email` step, then they are not asked to enter their email again (that is, an OTP is directly sent to them). However, until they actually complete the OTP flow, `getFactorsSetupForUser` does not return `["otp-email"]` as part of the output. -:::caution -In the edge case that a factor is active for a user, but they sign out before setting it up, then when they login next, SuperTokens still asks them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for TOTP auth), then users need to set up a device at that point in time. +:::warning[In the edge case that a factor is active for a user, but they sign out before setting it up, then when they login next, SuperTokens still asks them to complete the factor at that time. If SuperTokens doesn't have the required information (like no TOTP device for TOTP auth), then users need to set up a device at that point in time.] If you would like to change how this works and only want users to set up their factor via the settings page, and not during sign in, you can do this by overriding the `getMFARequirementsForAuth` function, which takes as an input the list of factors that are setup for the current user. ::: @@ -1444,11 +1434,11 @@ The subsequent sections in this doc walk through frontend setup, and also specif ### 2. Set up the frontend - + - + - + The pre-built UI provides support for the following MFA methods: - TOTP @@ -1458,115 +1448,106 @@ If you want other types of MFA (like magic links, or password), please consider We start by initialising the MFA recipe on the frontend and providing the list of first factors as shown below: - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" -import Passwordless from "supertokens-auth-react/recipe/passwordless" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" +import supertokens from "supertokens-auth-react"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - EmailPassword.init( /* ... */), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - }), - // highlight-start - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init(/* ... */), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless" -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIEmailPassword.init( /* ... */), - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - }), - // highlight-start - supertokensUIMultiFactorAuth.init({ - firstFactors: [ - supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, - supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init(/* ... */), + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + supertokensUIMultiFactorAuth.init({ + firstFactors: [ + supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, + supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, + ], + }), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; -import Session from 'supertokens-web-js/recipe/session' +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - Session.init(), - MultiFactorAuth.init() - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` + + - - - - -In the above snippet, `thirdparty` and email password are configured as first factors. The second factor is determined [on the backend](/docs/additional-verification/mfa/initial-setup#1-set-up-the-backend), based on the boolean value of [`v` in the MFA claim in the session](/docs/additional-verification/mfa/important-concepts#factors). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the `MFAInfo` endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example: +In the above snippet, `thirdparty` and email password are configured as first factors. The second factor is determined [on the backend](/additional-verification/mfa/initial-setup#1-set-up-the-backend), based on the boolean value of [`v` in the MFA claim in the session](/additional-verification/mfa/important-concepts#factors). If the `v` is `false` in the session, it means that there are still factors pending before the user has completed login. In this case, the frontend SDK calls the `MFAInfo` endpoint (see more about this later) on the backend which returns the list of factors (`string[]`) that the user must complete next. For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. -If you notice, in the above code snippet, `Passwordless.init` is also included, and this handles cases where the second factor is `otp-email` or `otp-sms`. For TOTP, a different recipe is used as shown later in this guide. +If you notice, in the above code snippet, `Passwordless.init` is also included, and this handles cases where the second factor is `otp-email` or `otp-phone`. For TOTP, a different recipe is used as shown later in this guide. - + - + -For a multi-factor setup, the first factors are selected based on [the configuration of the tenant](./backend-setup#multi-tenant-setup). Each tenant has a `firstFactors` array configuration which determines the login options shown for that tenant. For MFA, the login options are determined by the [`requiredSecondaryFactors` configuration on the tenant](./backend-setup#multi-tenant-setup-1), or based on the customisations for `getMFARequirementsForAuth` on the backend. +For a multi-factor setup, the first factors are selected based on [the configuration of the tenant](/additional-verification/mfa/initial-setup#12-configure-the-first-factors). Each tenant has a `firstFactors` array configuration which determines the login options shown for that tenant. For MFA, the login options are determined by the [`requiredSecondaryFactors` configuration on the tenant](/additional-verification/mfa/initial-setup#13-configure-the-second-factor), or based on the customisations for `getMFARequirementsForAuth` on the backend. To tell the frontend to dynamically load the factors based on the tenant, four things need to supply: - The current `tenantId` @@ -1574,144 +1555,130 @@ To tell the frontend to dynamically load the factors based on the tenant, four t - Add `MultiFactorAuth.init` to the recipe list without any configured `firstFactors` - Init all the recipes that can be possibly used by any tenant as the first or second factor. - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" -import Passwordless from "supertokens-auth-react/recipe/passwordless" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import Multitenancy from "supertokens-auth-react/recipe/multitenancy" +import supertokens from "supertokens-auth-react"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - Multitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - // highlight-start - getTenantId: (input) => { - // Implement the following based on the UX flow you want for - // tenant discovery - return "TODO.." - } - // highlight-end - } - } - } - }), - EmailPassword.init( /* ... */), - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - }), - // highlight-start - MultiFactorAuth.init() - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + // Implement the following based on the UX flow you want for + // tenant discovery + return "TODO.."; + }, + }; + }, + }, + }), + EmailPassword.init(/* ... */), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + MultiFactorAuth.init(), + ], +}); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless" -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - supertokensUIMultitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - // highlight-start - getTenantId: (input) => { - // Implement the following based on the UX flow you want for - // tenant discovery - return "TODO.." - } - // highlight-end - } - } - } - }), - supertokensUIEmailPassword.init( /* ... */), - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - }), - // highlight-start - supertokensUIMultiFactorAuth.init() - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + supertokensUIMultitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + // Implement the following based on the UX flow you want for + // tenant discovery + return "TODO.."; + }, + }; + }, + }, + }), + supertokensUIEmailPassword.init(/* ... */), + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", + }), + supertokensUIMultiFactorAuth.init(), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; -import Session from 'supertokens-web-js/recipe/session' +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - Session.init(), - MultiFactorAuth.init() - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - - - - + + - In the above code snippet, `ThirdPartyEmailPassword` and `Passwordless` are included as the auth methods. This works for a variety of use cases like: - - The first factor for any tenant can be third party or email password login, and the second factor can be passwordless login (`otp-email` or `otp-sms`). + - The first factor for any tenant can be third party or email password login, and the second factor can be passwordless login (`otp-email` or `otp-phone`). - The first factor for any tenant can be email password, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be third party, with, or without a second factor (like `otp-email`).. - The first factor for any tenant can be passwordless login (with magic link), with or without a second factor (like `otp-email`). - You can even change `passwordles.init` to using `thirdpartypasswordless.init` if you want to have the first factor for any tenant to be `thirdparty` or passwordless login, with or without a second factor (like `otp-email`). - The `MultiFactorAuth` is configured without any configured `firstFactors` because the frontend is set to dynamically load the first factors based on the tenant. Therefore, `usesDynamicLoginMethods: true` is included in the `SuperTokens.init` call. -- The `Multitenancy` is configured as well, and a skeleton for `getTenantId` is provided. You need to implement this function based on the UX flow desired for tenant discovery. For example, [here is a common UX flow in which the tenant ID is determined based on the current sub domain](/docs/authentication/enterprise/subdomain-login). +- The `Multitenancy` is configured as well, and a skeleton for `getTenantId` is provided. You need to implement this function based on the UX flow desired for tenant discovery. For example, [here is a common UX flow in which the tenant ID is determined based on the current sub domain](/authentication/enterprise/subdomain-login). -:::important -- If you do initialize the `firstFactors` array for `MultiFactorAuth.init()` on the frontend, it is not considered when `usesDynamicLoginMethods: true` is included. -- If the tenant doesn't have the `firstFactors` array set, then the list of first factors that appear is determined by the [login methods that are enabled in that tenant's configuration](/docs/multi-tenancy/new-tenant). +:::note[- If you do initialize the `firstFactors` array for `MultiFactorAuth.init()` on the frontend, it is not considered when `usesDynamicLoginMethods: true` is included.] +- If the tenant doesn't have the `firstFactors` array set, then the list of first factors that appear is determined by the [login methods that are enabled in that tenant's configuration](/authentication/enterprise/manage-tenants#create-a-new-tenant). ::: -The second factor for a tenant is selected based on the [`secondaryFactors` configuration for the tenant](./backend-setup#multi-tenant-setup-1), or based on any custom implementation for the `getMFARequirementsForAuth` function. If the current user has specific MFA methods enabled for them, those are also shown as options as well. Overall, the list of secondary factors is used to build the `next` array returned from the `MFAInfo` endpoint (see more about this later). For example: +The second factor for a tenant is selected based on the [`secondaryFactors` configuration for the tenant](/additional-verification/mfa/initial-setup#13-configure-the-second-factor), or based on any custom implementation for the `getMFARequirementsForAuth` function. If the current user has specific MFA methods enabled for them, those are also shown as options as well. Overall, the list of secondary factors is used to build the `next` array returned from the `MFAInfo` endpoint (see more about this later). For example: - If the next array is `["otp-email"]`, then the user sees the enter OTP screen for the email associated with the first factor login. - If the `n` array has multiple items, the user sees a [factor chooser screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/mfa-chooser--multiple-factors) using which they can decide which factor they want to continue with. - If the `next` is empty, it means that: @@ -1720,7 +1687,7 @@ The second factor for a tenant is selected based on the [`secondaryFactors` conf In the subsequent sections, specific MFA setup examples are given for your reference. - + #### Usage with email verification @@ -1729,95 +1696,87 @@ For example, if the user has email password as the first factor, and then TOTP a To switch the order such that email verification happens after the secondary factors of MFA, follow the next code snippet. - - - - + + ```tsx -import supertokens from "supertokens-auth-react" +import supertokens from "supertokens-auth-react"; import EmailVerification from "supertokens-auth-react/recipe/emailverification"; import Session from "supertokens-auth-react/recipe/session"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes... - EmailVerification.init({ - mode: "REQUIRED", - }), - Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // highlight-start - getGlobalClaimValidators: (input) => { - let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(v => v.id === EmailVerification.EmailVerificationClaim.id)!; - let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== EmailVerification.EmailVerificationClaim.id); - return [...filteredValidators, emailVerificationClaimValidator]; - } - // highlight-end - } - } - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes... + EmailVerification.init({ + mode: "REQUIRED", + }), + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getGlobalClaimValidators: (input) => { + let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find( + (v) => v.id === EmailVerification.EmailVerificationClaim.id, + )!; + let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter( + (v) => v.id !== EmailVerification.EmailVerificationClaim.id, + ); + return [...filteredValidators, emailVerificationClaimValidator]; + }, + }; + }, + }, + }), + ], +}); ``` - -In the snippet above, the `getGlobalClaimValidators` function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors. - - - - - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailVerification from "supertokens-auth-react-script/recipe/emailverification"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes... - supertokensUIEmailVerification.init({ - mode: "REQUIRED", - }), - supertokensUISession.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // highlight-start - getGlobalClaimValidators: (input) => { - let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find(v => v.id === supertokensUIEmailVerification.EmailVerificationClaim.id)!; - let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== supertokensUIEmailVerification.EmailVerificationClaim.id); - return [...filteredValidators, emailVerificationClaimValidator]; - } - // highlight-end - } - } - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes... + supertokensUIEmailVerification.init({ + mode: "REQUIRED", + }), + supertokensUISession.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getGlobalClaimValidators: (input) => { + let emailVerificationClaimValidator = input.claimValidatorsAddedByOtherRecipes.find( + (v) => v.id === supertokensUIEmailVerification.EmailVerificationClaim.id, + )!; + let filteredValidators = input.claimValidatorsAddedByOtherRecipes.filter( + (v) => v.id !== supertokensUIEmailVerification.EmailVerificationClaim.id, + ); + return [...filteredValidators, emailVerificationClaimValidator]; + }, + }; + }, + }, + }), + ], +}); ``` + + In the snippet above, the `getGlobalClaimValidators` function in the Session recipe is overridden to add the email verification validator at the end of the returned validators array. This ensures that post the first factor sign up, the first validator that fails is the MFA one which redirects the user to complete the MFA factors. - - - - #### Handle misconfigurations @@ -1834,45 +1793,46 @@ Sometimes, users may end up seeing [an access denied screen](https://master--657 You can override this component in the following way: - - - - - - + + +:::warning[You cannot override the pre-built UI in non react apps yet.] +::: + + + + + + ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; function App() { - return ( - - { - return ( -
- Access denied! - {props.error === undefined ? null : props.error} -
- ); - }, - }}> - {/* Rest of the JSX */} -
-
- ); + return ( + + { + return ( +
+ Access denied! + {props.error === undefined ? null : props.error} +
+ ); + }, + }} + > + {/* Rest of the JSX */} +
+
+ ); } export default App; ``` - -
- - - + + ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; @@ -1880,64 +1840,59 @@ import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui"; import { SessionComponentsOverrideProvider } from "supertokens-auth-react/recipe/session"; function App() { - if(canHandleRoute([/*...*/])){ - return ( - { - return ( -
- Access denied! - {props.error === undefined ? null : props.error} -
- ); - }, - }}> - {getRoutingComponent([/*...*/])} -
- ) - } + if ( + canHandleRoute([ + /*...*/ + ]) + ) { return ( - - {/* Rest of the JSX */} - + { + return ( +
+ Access denied! + {props.error === undefined ? null : props.error} +
+ ); + }, + }} + > + {getRoutingComponent([ + /*...*/ + ])} +
); + } + return {/* Rest of the JSX */}; } export default App; ``` +
+ + + -
- -
- -
- - - -:::caution -You cannot override the pre-built UI in non react apps yet. -::: + + - -
+ -
+ - - After the first factor sign in is over, to know the next auth challenge, the frontend should rely on the session's access token payload MFA claim's `n` array. For example, the access token payload may have the following content: ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -1945,170 +1900,136 @@ This means that the user has completed the email password login, and that there #### 1.1 Initialize the MFA recipe - - - - - + + +:::success[This step is not applicable for mobile apps. Please continue reading.] +::: + + + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-next-line - MultiFactorAuth.init() - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + ], }); ``` - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-next-line - supertokensMultiFactorAuth.init() - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + supertokensMultiFactorAuth.init(), + ], }); ``` + + + + - - - - - - - - - -:::success -This step is not applicable for mobile apps. Please continue reading. -::: + + - - #### 1.2 Add the MFA flow The overall lifecycle of a factor post sign in is as follows: - -## Asking for the first factor - + This is the same as setting up a recipe per the other recipe guides. Please follow those. - -## Checking the `v` boolean value in the MFA claim - + + After the first factor is complete, the frontend needs to check if there are any pending MFA challenges. This can be done by reading the `v` claim from the session as shown below: - - - - - - + + + + ```tsx -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Session from "supertokens-web-js/recipe/session"; async function isAllMFACompleted() { - if (await Session.doesSessionExist()) { - let mfaClaim = await Session.getClaimValue({ - claim: MultiFactorAuth.MultiFactorAuthClaim - }); - if (mfaClaim === undefined) { - // this can happen during migration where the session is an older one - // that was created before MFA was introduced on the backend - return true; - } else { - return mfaClaim.v - } + if (await Session.doesSessionExist()) { + let mfaClaim = await Session.getClaimValue({ + claim: MultiFactorAuth.MultiFactorAuthClaim, + }); + if (mfaClaim === undefined) { + // this can happen during migration where the session is an older one + // that was created before MFA was introduced on the backend + return true; } else { - throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + return mfaClaim.v; } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); + } } ``` - - - - - -```tsx -import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function isAllMFACompleted() { - if (await supertokensSession.doesSessionExist()) { - let mfaClaim = await supertokensSession.getClaimValue({ - claim: supertokensMultiFactorAuth.MultiFactorAuthClaim - }); - if (mfaClaim === undefined) { - // this can happen during migration where the session is an older one - // that was created before MFA was introduced on the backend - return true; - } else { - return mfaClaim.v - } + if (await supertokensSession.doesSessionExist()) { + let mfaClaim = await supertokensSession.getClaimValue({ + claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, + }); + if (mfaClaim === undefined) { + // this can happen during migration where the session is an older one + // that was created before MFA was introduced on the backend + return true; } else { - throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + return mfaClaim.v; } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); + } } ``` - - - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function isAllMFACompleted() { - if (await SuperTokens.doesSessionExist()) { - - // highlight-start - let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; - return isMFACompleted - // highlight-end - } + if (await SuperTokens.doesSessionExist()) { + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + return isMFACompleted; + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -2122,11 +2043,8 @@ class MainApplication: Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -2147,11 +2065,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -2170,30 +2085,27 @@ Future isAllMFACompleted() async { return false; // Return false if "st-mfa" is not present or "v" is not found } ``` + + + + - - - - - - - - -## Checking the `next` array -Once it is verified that MFA is still pending, the list of factors the user must do next needs to be retrieved. This can be done by calling the [MFA Info endpoint](#mfa-info-endpoint) which returns a list of next (`string[]`) factors: + + +Once it is verified that MFA is still pending, the list of factors the user must do next needs to be retrieved. This can be done by calling the [MFA Info endpoint](#the-mfa-info-endpoint) which returns a list of next (`string[]`) factors: - If there are multiple values in this array, then the frontend needs to show these options to the user and ask them to pick one of them. - If there is only one item, then the UI can directly ask the user to complete that factor. - If this array is empty, then: - A misconfiguration exists on the backend. This would show an access denied screen to the user. OR; - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `backend`'s `checkAllowedToSetupFactorElseThrowInvalidClaimError` function to not allow a factor setup until the email has been verified. - -## Checking for factor setup + + Once the user has picked a specific factor (or if `next` contains only one item), you need to check if that factor has already been setup for that user. A factor is setup already if: - For `totp`: The user has already added a `totp` device and verified it. - For `otp-email`: The user has a passwordless `loginMethod` that has an email associated with it. - For `link-email`: The user has a passwordless `loginMethod` that has an email associated with it. Note that this is not a valid secondary factor, but is a valid first factor. -- For `otp-sms`: The user has a passwordless `loginMethod` that has a phone number associated with it. +- For `otp-phone`: The user has a passwordless `loginMethod` that has a phone number associated with it. - For `link-phone`: The user has a passwordless `loginMethod` that has a phone number associated with it. Note that this is not a valid secondary factor, but is a value first factor. - For `emailpassword`: The user has an email password `loginMethod`. - For `thirdparty`: The user has a third party `loginMethod`. @@ -2201,22 +2113,22 @@ Once the user has picked a specific factor (or if `next` contains only one item) If the user has the factor already setup, you can skip the setup step and directly ask them for the challenge: - For `totp`: Ask them to enter the OTP. - For `otp-email`: Send them an email with the OTP, and ask them to enter the OTP. -- For `otp-sms`: Send them an SMS with the OTP, and ask them to enter the OTP. +- For `otp-phone`: Send them an SMS with the OTP, and ask them to enter the OTP. - For `emailpassword`: Ask them to enter their password. - For `thirdparty`: Ask them to login using the third party provider. In case the user does not have the factor setup, you need to ask them to set it up first: - For `totp`: Ask them to scan the QR code and enter the TOTP to verify the device. - For `otp-email`: Ask them to enter their email and send them an email with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. -- For `otp-sms`: Ask them to enter their phone number and send them an SMS with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's phone number from another login method (see later), you do not need to ask them to enter their phone number again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. +- For `otp-phone`: Ask them to enter their phone number and send them an SMS with the OTP. Once they enter the OTP, a passwordless user is created and associated with their user object. Note that if you already have the user's phone number from another login method (see later), you do not need to ask them to enter their phone number again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. - For `emailpassword`: Ask them to enter their email and password. Once they enter the password, an email password user is created and associated with their user object. Note that if you already have the user's email from another login method (see later), you do not need to ask them to enter their email again. In that way, it would be similar to as if the factor is already setup, but technically, it is not. Here you would be calling the sign up API, vs in the other case (where the factor is already setup), you would be calling the sign in API. - For `thirdparty`: Ask them to login using the third party provider. Once they login, a third party user is created and associated with their user object. -In the later guides of this recipe, the use cases are described. If you want to know the status of any factor, you can get that by calling the [MFA Info endpoint](#mfa-info-endpoint). - +In the later guides of this recipe, the use cases are described. If you want to know the status of any factor, you can get that by calling the [MFA Info endpoint](#the-mfa-info-endpoint). + - + ## References @@ -2231,131 +2143,125 @@ This is an important endpoint which can be utilized to: Our pre-built UI uses this API automatically, but you can also always call this API manually if you are building a custom UI: - - - - + + +Call the following API when you want to know the status of any factor. Notice that the API call requires the session's access token as an input (this should be included by the frontend SDK automatically): + + + + + + ```tsx -import MultifactorAuth from "supertokens-web-js/recipe/multifactorauth" -import Session from "supertokens-web-js/recipe/session" +import MultifactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import Session from "supertokens-web-js/recipe/session"; async function fetchMFAInfo() { - if (await Session.doesSessionExist()) { - try { - let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo() - let factorEmails = mfaInfo.emails; - let factorPhoneNumbers = mfaInfo.phoneNumbers; - - let emailsForOTPEmail = factorEmails["otp-email"]; - let phoneNumbersForOTPSms = factorEmails["otp-sms"]; - let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); - let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); - let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); - let next = mfaInfo.factors.next; - - let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + if (await Session.doesSessionExist()) { + try { + let mfaInfo = await MultifactorAuth.resyncSessionAndFetchMFAInfo(); + let factorEmails = mfaInfo.emails; + let factorPhoneNumbers = mfaInfo.phoneNumbers; + + let emailsForOTPEmail = factorEmails["otp-email"]; + let phoneNumbersForOTPPhone = factorPhoneNumbers["otp-phone"]; + let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); + let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); + let isOTPPhoneSetup = mfaInfo.factors.alreadySetup.includes("otp-phone"); + let next = mfaInfo.factors.next; + + let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); + } } ``` - - - - - -```tsx -import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth" -import supertokensSession from "supertokens-web-js-script/recipe/session" + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchMFAInfo() { - if (await supertokensSession.doesSessionExist()) { - try { - let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo() - let factorEmails = mfaInfo.emails; - let factorPhoneNumbers = mfaInfo.phoneNumbers; - - let emailsForOTPEmail = factorEmails["otp-email"]; - let phoneNumbersForOTPSms = factorEmails["otp-sms"]; - let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); - let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); - let isOTPSmsSetup = mfaInfo.factors.alreadySetup.includes("otp-sms"); - let next = mfaInfo.factors.next; - - let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: For first factor setup, you do not need to call this function") + if (await supertokensSession.doesSessionExist()) { + try { + let mfaInfo = await supertokensMultiFactorAuth.resyncSessionAndFetchMFAInfo(); + let factorEmails = mfaInfo.emails; + let factorPhoneNumbers = mfaInfo.phoneNumbers; + + let emailsForOTPEmail = factorEmails["otp-email"]; + let phoneNumbersForOTPPhone = factorPhoneNumbers["otp-phone"]; + let isTotpSetup = mfaInfo.factors.alreadySetup.includes("totp"); + let isOTPEmailSetup = mfaInfo.factors.alreadySetup.includes("otp-email"); + let isOTPPhoneSetup = mfaInfo.factors.alreadySetup.includes("otp-phone"); + let next = mfaInfo.factors.next; + + let factorsAllowedToBeSetup = mfaInfo.factors.allowedToSetup; + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: For first factor setup, you do not need to call this function"); + } } ``` + + + + +```bash +curl --location --request PUT '/auth/mfa/info' \ +--header 'Authorization: Bearer ...' +``` + + - - - - + + - In the above code snippet, the list of factors which the user must complete next (in the `next` array) is retrieved along with all the relevant information to know what state each factor is in to decide if the user should be prompted to setup the factor (for example create a new TOTP device), or solve the auth challenge instead (for example, showing the enter TOTP screen). - The function is called `resyncSessionAndFetchMFAInfo` because it does two things: - fetches the MFA info that you can consume to know the `next` array and what state each factor is in. - resynchronizes the value of the `v` and `c` in the session's MFA claim. + + - - - - - - -Call the following API when you want to know the status of any factor. Notice that the API call requires the session's access token as an input (this should be included by the frontend SDK automatically): - -```bash -curl --location --request PUT '^{appInfo.apiDomain}^{appInfo.apiBasePath}/mfa/info' \ ---header 'Authorization: Bearer ...' -``` - - - - - The structure of the raw JSON response is as follows: ```json { - "status": "OK", - "factors": { - "alreadySetup": ["totp", "otp-email", "..."], - "allowedToSetup": ["otp-sms", "otp-email", "..."], - "next": ["otp-sms", "..."] - }, - "emails": { - "otp-email": ["user1@example.com", "user2@example.com"], - "link-email": ["user1@example.com", "user2@example.com"], - }, - "phoneNumbers": { - "otp-sms": ["+1234567890", "+1098765432"], - "link-phone": ["+1234567890", "+1098765432"], - }, + "status": "OK", + "factors": { + "alreadySetup": ["totp", "otp-email", "..."], + "allowedToSetup": ["otp-phone", "otp-email", "..."], + "next": ["otp-phone", "..."] + }, + "emails": { + "otp-email": ["user1@example.com", "user2@example.com"], + "link-email": ["user1@example.com", "user2@example.com"] + }, + "phoneNumbers": { + "otp-phone": ["+1234567890", "+1098765432"], + "link-phone": ["+1234567890", "+1098765432"] + } } ``` - - `factors.alreadySetup` is an array that contains all factors that have been setup by the user. If the current factor is a part of this array, it means that you can directly take the user to the factor challenge screen. If your factor depends on an email or phone number (like in the case of `otp-sms` or `otp-email`), then you can find the email to send the code to in the `emails` or `phoneNumbers` object in the response with the key as the current factor ID. + - `factors.alreadySetup` is an array that contains all factors that have been setup by the user. If the current factor is a part of this array, it means that you can directly take the user to the factor challenge screen. If your factor depends on an email or phone number (like in the case of `otp-phone` or `otp-email`), then you can find the email or phone number in the `emails` or `phoneNumbers` object in the response, keyed by the current factor ID. - `factors.allowedToSetup` is an array that contains all factors that the user can setup at this point. This is not that useful during the sign in process, but may be useful post sign in if you want to know what are the factors that the user can setup at any point in time. - `emails` is an object in which the key are all the factor IDs supported by SuperTokens (and any custom factor ID added by you). The values against each of the keys is a list of emails that can be utilized to complete the factor. The first email (index 0) in the list is the preferred email to use for the factor. The order is determined based on the first factor chosen by the user, and if the factor was already setup or not. @@ -2373,87 +2279,84 @@ curl --location --request PUT '^{appInfo.apiDomain}^{appInfo.apiBasePath}/mfa/in - Another claim needs to satisfy first (like email verification), before the next MFA challenge can display. This can happen if you configure the `checkAllowedToSetupFactorElseThrowInvalidClaimError` function, on the backend, to not allow a factor setup until the email is verified. - - ### Handle support cases Some situations exist in which users may be locked out of their accounts and would need you to do certain steps to unlock their accounts. These cases are: - - -## `ERR_CODE_009` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { - "status": "SIGN_IN_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)" + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_009)" } ``` - This can happen if the email password account you are trying to do MFA with is not verified. - -## `ERR_CODE_010` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { - "status": "SIGN_IN_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)" + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_010)" } ``` - This can happen if the email password account you are trying to do MFA with is already linked to another primary user that is not equal to the session user. - -## `ERR_CODE_011` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { - "status": "SIGN_IN_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)" + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_011)" } ``` - This can happen if the email password account you are trying to do MFA cannot link to the session user because there already exists another primary user with the same email. - -## `ERR_CODE_012` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signin POST`. - Output JSON: ```json { - "status": "SIGN_IN_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)" + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_012)" } ``` - To link the email password user with the session user, it must be confirmed that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error is sent to the frontend. - -## `ERR_CODE_013` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_013)" } ``` - An example scenario of when in the following scenario: - A user signs up with their phone number and OTP - Post sign up, they are prompted to add their email and a password for the account. In this case, since the entered email is not verified, this error will display. -- To resolve this, it is advised to change the flow to first ask the user to go through the email OTP flow post the first factor sign up, and then add a password to the account. This way, the email will be verified. - -## `ERR_CODE_014` +- To resolve this, it is advised to change the flow to first ask the user to go through the email OTP flow post the first factor sign up, and then add a password to the account. This way, the email will be verified. + + - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: @@ -2462,15 +2365,15 @@ These cases are: - The user logs out, and then creates a social login account with email `e1`. Then, they are prompted to add a password to this account. Since an email password account with `e1` already exists, SuperTokens will try and link that to this new account, but fail, since the email password account with `e1` is already a primary user. - To resolve this, it is advised to manually link the `e1` social login account with the `e1` email password account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - -## `ERR_CODE_015` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: @@ -2480,15 +2383,15 @@ These cases are: - This will cause this type of error since the linking of the new social login and email account will fail since there already exists another primary user with the same (`e1`) email. - To resolve this, it is advised not allowing users to specify an email when asking them to add a password for their account. - -## `ERR_CODE_016` + + - This can happen when the second factor is `emailpassword`: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: @@ -2497,42 +2400,43 @@ These cases are: - The user logs out and creates another social login account with email `e1` (say `GitHub`), and then tries and adds a password to this account with email `e1`. Here, SuperTokens will try and make the `GitHub` login a primary user, but fail, since the email `e1` is already a primary user (with Google login). - To resolve this, it is advised to manually link the `e1` `GitHub` social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - -## `ERR_CODE_017` + + - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_017)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because the passwordless account is already linked with another primary user. - -## `ERR_CODE_018` + + - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code/consume POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_018)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but it can't because there exists another primary user with the same email as the passwordless account. - -## `ERR_CODE_019` + + - This can happen when the second factor relies on the passwordless recipe. - API Path is `/signinup/code POST` or `/signinup/code/consume POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_019)" } ``` - This can happen when the passwordless account is trying to link to the account of the first factor, but, the first factor account cannot become a primary user because there exists another account with the same email as the first factor user account which is already primary. + @@ -2562,11 +2466,10 @@ SuperTokens enforces that a user has completed all the required factors by keepi ## See also - - - - - - - - + + + + + + + diff --git a/docs/additional-verification/mfa/introduction.mdx b/docs/additional-verification/mfa/introduction.mdx index 3a47e4a51b..be6fda4efa 100644 --- a/docs/additional-verification/mfa/introduction.mdx +++ b/docs/additional-verification/mfa/introduction.mdx @@ -1,24 +1,28 @@ --- -sidebar_position: 1 -title: Introduction -hide_title: true -skip_llms_txt: true -description: >- - Implement multi-factor authentication with email, SMS, or TOTP, and customize - user authentication preferences. -page_type: overview -recipe: mfa -category: multi-factor-authentication +title: Multi-Factor Authentication +description: Add email or SMS OTP, TOTP, or WebAuthn passkeys as MFA factors and require step-up authentication for sensitive actions. +sidebar: + label: Overview + order: 10 --- + -# Multi-factor authentication +## MFA summary + +- SuperTokens supports email or SMS OTP, TOTP, and WebAuthn or Passkeys as MFA factors. +- Require an additional challenge for sensitive routes or actions with step-up authentication. +- Magic links work only as a first factor with the prebuilt UI because the link may open on another device. Use email or SMS OTP as a later factor instead. +- MFA is available for managed SuperTokens deployments. + + ## Overview Multi-factor authentication (MFA) is a security process that requires users to verify their identity through multiple forms of credentials before gaining access to a system. **SuperTokens** allows you to integrate MFA in your application using Email/SMS One-Time Password (OTP), Time-based One-Time Password (TOTP), or WebAuthn/Passkeys. + ## Prerequisites @@ -42,95 +46,43 @@ Besides that, you can check the initial quickstart guide for step-by-step instru Before you explore a guide, read through the **Important Concepts** page first. It explains multiple topics that get used in each tutorial. - - - - Important Concepts - - - Go through a quick explanation of how MFA works and some common terminologies. - - - - - Quickstart Guide - - - Implement an authentication flow that uses MFA. - - - - - Implement Step Up Authentication - - - Require additional authentication challenges on specific routes or actions. - - - - - Implement Recovery Codes - - - Allow users to recover their account if they lose access to one of the factors. - - - + + +Go through a quick explanation of how MFA works and some common terminologies. + + +Implement an authentication flow that uses MFA. + + +Require additional authentication challenges on specific routes or actions. + + +Allow users to recover their account if they lose access to one of the factors. + + ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Require TOTP for all users - - - Force all users to use TOTP. - - - - - Require TOTP for specific users - - - Enable TOTP only for some of the users. - - - - - Require OTP for all users - - - Force all users to use OTP. - - - - - Require OTP for specific users - - - Enable OTP only for some of the users. - - - - - WebAuthn - - - Use WebAuthn as a secondary factor - - - - - Protect frontend and backend routes - - - Check for the MFA status on specific routes. - - - - - + + +Force all users to use TOTP. + + +Enable TOTP only for some of the users. + + +Force all users to use OTP. + + +Enable OTP only for some of the users. + + +Use WebAuthn as a secondary factor + + +Check for the MFA status on specific routes. + + diff --git a/docs/additional-verification/mfa/legacy-mfa/_category_.json b/docs/additional-verification/mfa/legacy-mfa/_category_.json deleted file mode 100644 index cb389d680e..0000000000 --- a/docs/additional-verification/mfa/legacy-mfa/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Legacy method", - "position": 11 -} diff --git a/docs/additional-verification/mfa/legacy-mfa/backend-setup/_category_.json b/docs/additional-verification/mfa/legacy-mfa/backend-setup/_category_.json deleted file mode 100644 index f540a9b0d8..0000000000 --- a/docs/additional-verification/mfa/legacy-mfa/backend-setup/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Backend Setup", - "position": 3 -} diff --git a/docs/additional-verification/mfa/legacy-mfa/backend-setup/first-factor.mdx b/docs/additional-verification/mfa/legacy-mfa/backend-setup/first-factor.mdx index 6f97e59b81..3ae7fe3e35 100644 --- a/docs/additional-verification/mfa/legacy-mfa/backend-setup/first-factor.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/backend-setup/first-factor.mdx @@ -1,242 +1,200 @@ --- -id: first-factor title: Setting up the first factor -hide_title: true -sidebar_position: 1 -description: >- - Set up the first authentication factor using SuperTokens with various - frameworks. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Set up the first authentication factor using SuperTokens with various frameworks. +sidebar: + hidden: true + order: 1 --- - -# Setting up the 1st factor - ## 1. Initialisation Start by following the recipe guide for the first factor. In this guide, we will take the example of `thirdparty` and `emailpassword` recipes as being the first factor. -After following the [backend quick setup section](/docs/quickstart/backend-setup) (or any of the framework specific integration guides), you should have all the auth APIs exposed to the frontend via the SuperTokens middleware. The `supertokens.init` code on the server would look like this: - - +After following the [backend quick setup section](/quickstart#2-integrate-the-backend-sdk) (or any of the framework specific integration guides), you should have all the auth APIs exposed to the frontend via the SuperTokens middleware. The `supertokens.init` code on the server would look like this: - - - - - + + + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - framework: "express", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init(), // initializes session features - UserMetadata.init() // initializes the user metadata feature - ] + framework: "express", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init(), // initializes session features + UserMetadata.init(), // initializes the user metadata feature + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init(), // initializes session features - UserMetadata.init() // initializes the user metadata feature - ] + framework: "hapi", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init(), // initializes session features + UserMetadata.init(), // initializes the user metadata feature + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init(), // initializes session features - UserMetadata.init() // initializes the user metadata feature - ] + framework: "fastify", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init(), // initializes session features + UserMetadata.init(), // initializes the user metadata feature + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - framework: "koa", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init(), // initializes session features - UserMetadata.init() // initializes the user metadata feature - ] + framework: "koa", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init(), // initializes session features + UserMetadata.init(), // initializes the user metadata feature + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init(), // initializes session features - UserMetadata.init() // initializes the user metadata feature - ] + framework: "loopback", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init(), // initializes session features + UserMetadata.init(), // initializes the user metadata feature + ], }); ``` - -// -// -// :::important -// Please refer the **serverless deployment** section in the ThirdParty + EmailPassword recipe guide -// ::: -// -// -// -// -// :::important -// Please refer the **NextJS** section in the ThirdParty + EmailPassword recipe guide -// ::: -// -// -// -// -// :::important -// Please refer the **NestJS** section in the ThirdParty + EmailPassword recipe guide -// ::: -// -// - - - - - -```go showAppTypeSelect + + + + +```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" @@ -248,17 +206,17 @@ import ( ) func main() { - apiBasePath := "^{appInfo.apiBasePath}" - websiteBasePath := "^{appInfo.websiteBasePath}" + apiBasePath := "/auth" + websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "^{coreInfo.uri}", - APIKey: "^{coreInfo.key}", + ConnectionURI: "", + APIKey: "", }, AppInfo: supertokens.AppInfo{ - AppName: "^{appInfo.appName}", - APIDomain: "^{appInfo.apiDomain}", - WebsiteDomain: "^{appInfo.websiteDomain}", + AppName: "", + APIDomain: "", + WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, @@ -275,27 +233,25 @@ func main() { } } ``` - - - - - - + + + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, usermetadata init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='fastapi', recipe_list=[ @@ -311,25 +267,23 @@ init( mode='asgi' # use wsgi if you are running using gunicorn ) ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, usermetadata init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='flask', recipe_list=[ @@ -344,25 +298,23 @@ init( ] ) ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, usermetadata init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='django', recipe_list=[ @@ -378,23 +330,42 @@ init( mode='asgi' # use wsgi if you are running django server in sync mode ) ``` + + + + + + + +{/* + +:::note +Please refer the **serverless deployment** section in the ThirdParty + EmailPassword recipe guide +::: + + +:::note +Please refer the **NextJS** section in the ThirdParty + EmailPassword recipe guide +::: + + +:::note +Please refer the **NestJS** section in the ThirdParty + EmailPassword recipe guide +::: + +*/} + + - - - - - -:::important -You should have also added the SuperTokens `middleware` and `errorHandler` (depending on your framework) to your application. We are not showing it in the above code snippet for brevity, but it is explained in the ThirdParty + EmailPassword recipe guide. +:::note[You should have also added the SuperTokens `middleware` and `errorHandler` (depending on your framework) to your application. We are not showing it in the above code snippet for brevity, but it is explained in the ThirdParty + EmailPassword recipe guide.] ::: ## 2. Adding second factor claim After sign up or sign in of the first factor, the existence of the session signifies the completion of the first factor, but we want to explicitly mark the second factor as incomplete. This can be done by overriding the `createNewSession` function in the `Session.init` config: - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import { BooleanClaim } from "supertokens-node/recipe/session/claims"; @@ -403,38 +374,39 @@ import { BooleanClaim } from "supertokens-node/recipe/session/claims"; This will be used to modify the session's access token payload to add {"2fa-completed": false} into it. */ -// highlight-start export const SecondFactorClaim = new BooleanClaim({ - fetchValue: () => false, - key: "2fa-completed", + fetchValue: () => false, + key: "2fa-completed", }); -// highlight-end Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - /* This function is called after signing in or signing up via the first factor */ - createNewSession: async function (input) { - return originalImplementation.createNewSession({ - ...input, - accessTokenPayload: { - ...input.accessTokenPayload, - // highlight-next-line - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), - }, - }); - }, - }; + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + /* This function is called after signing in or signing up via the first factor */ + createNewSession: async function (input) { + return originalImplementation.createNewSession({ + ...input, + accessTokenPayload: { + ...input.accessTokenPayload, + ...(await SecondFactorClaim.build( + input.userId, + input.recipeUserId, + input.tenantId, + undefined, + input.userContext, + )), + }, + }); }, + }; }, -}) + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -444,11 +416,9 @@ import ( ) func main() { - // highlight-start SecondFactorClaim, _ := claims.BooleanClaim("2fa-completed", func(userId, tenantId string, userContext supertokens.UserContext) (interface{}, error) { return false, nil }, nil) - // highlight-end session.Init(&sessmodels.TypeInput{ Override: &sessmodels.OverrideStruct{ @@ -456,7 +426,6 @@ func main() { oCreateNewSession := *originalImplementation.CreateNewSession /* This function is called after signing in or signing up via the first factor */ (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // highlight-start if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} } @@ -464,7 +433,6 @@ func main() { if err != nil { return nil, err } - // highlight-end return oCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation @@ -473,10 +441,8 @@ func main() { }) } ``` - - - - + + ```python from supertokens_python.recipe import session from supertokens_python.recipe.session.claims import BooleanClaim @@ -486,11 +452,9 @@ from supertokens_python.types import RecipeUserId # This will be used to modify the session's access token payload # to add {"2fa-completed": false} into it. -# highlight-start SecondFactorClaim = BooleanClaim( key="2fa-completed", fetch_value=lambda _, __, ___, ____, _____: False ) -# highlight-end def override_session_functions(original_implementation: RecipeInterface): @@ -510,7 +474,6 @@ def override_session_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # highlight-next-line access_token_payload = { **access_token_payload, **( @@ -538,12 +501,10 @@ def override_session_functions(original_implementation: RecipeInterface): session.init( - # highlight-next-line override=session.InputOverrideConfig(functions=override_session_functions) ) ``` - - - + + We add `SecondFactorClaim` into the access token payload. This will be set to false on session creation (see `fetchValue` in the claim definition). diff --git a/docs/additional-verification/mfa/legacy-mfa/backend-setup/meta.ts b/docs/additional-verification/mfa/legacy-mfa/backend-setup/meta.ts new file mode 100644 index 0000000000..6b931eb635 --- /dev/null +++ b/docs/additional-verification/mfa/legacy-mfa/backend-setup/meta.ts @@ -0,0 +1,6 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Backend Setup", + order: 3, +}); diff --git a/docs/additional-verification/mfa/legacy-mfa/backend-setup/protecting-api.mdx b/docs/additional-verification/mfa/legacy-mfa/backend-setup/protecting-api.mdx index 8b81838de0..f92a805f04 100644 --- a/docs/additional-verification/mfa/legacy-mfa/backend-setup/protecting-api.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/backend-setup/protecting-api.mdx @@ -1,52 +1,38 @@ --- -id: protecting-api title: Protecting API routes -hide_title: true -sidebar_position: 3 -description: >- - Protect API routes by enforcing 2FA using session claim validators in various - programming languages. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Protect API routes by enforcing 2FA using session claim validators in various programming languages. +sidebar: + hidden: true + order: 3 --- - -# Protecting API routes - In the previous steps, we saw the a session is created after the first factor, with `SecondFactorClaim` set to false, and then after the second factor is completed, we update that value to true. ## 1. Protecting all APIs We want to protect all the application APIs such that they are accessible only when `SecondFactorClaim` is `true` - indicating that the user has completed 2FA. We can do this by by overriding the `getGlobalClaimValidators` function in the Session recipe. - - - -```tsx + + +```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import Session from "supertokens-node/recipe/session"; -import { BooleanClaim } from "supertokens-node/recipe/session/claims"; // REMOVE_FROM_OUTPUT -declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - getGlobalClaimValidators: (input) => [ - ...input.claimValidatorsAddedByOtherRecipes, - SecondFactorClaim.validators.hasValue(true), - ], - }; - }, - } -}) - + override: { + functions: (oI) => { + return { + ...oI, + getGlobalClaimValidators: (input) => [ + ...input.claimValidatorsAddedByOtherRecipes, + SecondFactorClaim.validators.hasValue(true), + ], + }; + }, + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -57,23 +43,19 @@ import ( func main() { - // highlight-start _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId, tenantId string, userContext supertokens.UserContext) (interface{}, error) { return false, nil }, nil) - // highlight-end session.Init(&sessmodels.TypeInput{ Override: &sessmodels.OverrideStruct{ Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { - // highlight-start (*originalImplementation.GetGlobalClaimValidators) = func(userId string, claimValidatorsAddedByOtherRecipes []claims.SessionClaimValidator, tenantId string, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { claimValidatorsAddedByOtherRecipes = append(claimValidatorsAddedByOtherRecipes, SecondFactorClaimValidator.HasValue(true, nil, nil)) return claimValidatorsAddedByOtherRecipes, nil } - // highlight-end return originalImplementation }, @@ -81,10 +63,8 @@ func main() { }) } ``` - - - - + + ```python from typing import List, Dict, Any from supertokens_python.recipe.session.claims import BooleanClaim @@ -102,7 +82,6 @@ SecondFactorClaim = BooleanClaim( def override_session_functions(original_implementation: RecipeInterface): - # highlight-start async def get_global_claim_validators( tenant_id: str, user_id: str, @@ -114,49 +93,46 @@ def override_session_functions(original_implementation: RecipeInterface): SecondFactorClaim.validators.has_value(True) ] - # highlight-end original_implementation.get_global_claim_validators = get_global_claim_validators return original_implementation -session.init(override=session.InputOverrideConfig(override_session_functions)) +session.init( + override=session.InputOverrideConfig(functions=override_session_functions) +) ``` - - - + + ## 2. Protecting specific API routes If instead, you want to enforce 2FA on certain API routes, you can add the validator only when calling the `verifySession` function: - - - -```tsx + + +```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -import { BooleanClaim } from "supertokens-node/recipe/session/claims"; // REMOVE_FROM_OUTPUT -declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT let app = express(); -app.post("/like-comment", verifySession({ - // highlight-start +app.post( + "/like-comment", + verifySession({ overrideGlobalClaimValidators: (globalValidators) => [ - ...globalValidators, - SecondFactorClaim.validators.hasValue(true), - ] - // highlight-end -}), (req: SessionRequest, res) => { + ...globalValidators, + SecondFactorClaim.validators.hasValue(true), + ], + }), + (req: SessionRequest, res) => { //.... -}); + }, +); ``` - - - - + + ```go import ( "net/http" @@ -168,11 +144,9 @@ import ( ) func main() { - // highlight-start _, SecondFactorClaimValidator := claims.BooleanClaim("2fa-completed", func(userId, tenantId string, userContext supertokens.UserContext) (interface{}, error) { return false, nil }, nil) - // highlight-end http.ListenAndServe("SERVER ADDRESS", corsMiddleware( supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -180,13 +154,11 @@ func main() { if r.URL.Path == "/like-comment" { session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, SecondFactorClaimValidator.HasValue(true, nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, likeCommentAPI).ServeHTTP(rw, r) return } @@ -203,10 +175,9 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { // If it comes here, the user has completed 2fa. } ``` - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.session import SessionContainer from fastapi import Depends @@ -217,15 +188,13 @@ SecondFactorClaim = BooleanClaim( ) -@app.post("/like_comment") # type: ignore +@app.post("/like_comment") async def like_comment( session: SessionContainer = Depends( verify_session( - # highlight-start # We add the SecondFactorClaim's has_value(True) validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [SecondFactorClaim.validators.has_value(True)] - # highlight-end ) ) ): @@ -233,10 +202,9 @@ async def like_comment( pass ``` + + - - - -:::info Important +:::info[Important] If the `SecondFactorClaim` claim validator fails, then the SDK will send a `403` response. ::: diff --git a/docs/additional-verification/mfa/legacy-mfa/backend-setup/second-factor.mdx b/docs/additional-verification/mfa/legacy-mfa/backend-setup/second-factor.mdx index 25975f66a8..eb0a3f9e05 100644 --- a/docs/additional-verification/mfa/legacy-mfa/backend-setup/second-factor.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/backend-setup/second-factor.mdx @@ -1,266 +1,253 @@ --- -id: second-factor title: Setting up the second factor -hide_title: true -sidebar_position: 2 -description: >- - Configure SMS OTP as a second authentication factor using the Passwordless - recipe. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Configure SMS OTP as a second authentication factor using the Passwordless recipe. +sidebar: + hidden: true + order: 2 --- - -# Setting up the 2nd factor - ## 1. Initialisation -We use the [Passwordless recipe](https://supertokens.com/docs/passwordless/introduction) with SMS OTP as the second factor. You can follow the recipe's [backend quick setup guide](https://supertokens.com/docs/passwordless/quick-setup/backend) to configure a different method as well (for example with email magic links). +We use the [Passwordless recipe](https://supertokens.com/docs/authentication/passwordless/initial-setup) with SMS OTP as the second factor. You can follow the recipe's [backend quick setup guide](https://supertokens.com/docs/authentication/passwordless/initial-setup) to configure a different method as well (for example with email magic links). The `Passwordless.init` function should look something like this: - - - - - - - + + + + +:::note[Please refer the **serverless deployment** section in the Passwordless recipe guide] +::: + + +:::note[Please refer the **NextJS** section in the Passwordless recipe guide] +::: + + +:::note[Please refer the **NestJS** section in the Passwordless recipe guide] +::: + + + + + + + + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ - framework: "express", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE" - }), - // highlight-end - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init({/*Override from previous step*/}) - ] + framework: "express", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + }), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init({ + /*Override from previous step*/ + }), + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ - framework: "hapi", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE" - }), - // highlight-end - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init({ /*Override from previous step*/ }) - ] + framework: "hapi", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + }), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init({ + /*Override from previous step*/ + }), + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ - framework: "fastify", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE" - }), - // highlight-end - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init({/*Override from previous step*/}) - ] + framework: "fastify", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + }), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init({ + /*Override from previous step*/ + }), + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ - framework: "koa", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE" - }), - // highlight-end - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init({/*Override from previous step*/}) - ] + framework: "koa", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + }), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init({ + /*Override from previous step*/ + }), + ], }); ``` - - - - + + ```tsx import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import Passwordless from "supertokens-node/recipe/passwordless" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Passwordless from "supertokens-node/recipe/passwordless"; supertokens.init({ - framework: "loopback", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE" - }), - // highlight-end - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - Session.init({/*Override from previous step*/}) - ] + framework: "loopback", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + }), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + Session.init({ + /*Override from previous step*/ + }), + ], }); ``` - - - -:::important -Please refer the **serverless deployment** section in the Passwordless recipe guide -::: - - - + + -:::important -Please refer the **NextJS** section in the Passwordless recipe guide -::: + + - - + + -:::important -Please refer the **NestJS** section in the Passwordless recipe guide -::: - - - - - - -```go showAppTypeSelect + + + + +```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" @@ -274,29 +261,27 @@ import ( ) func main() { - apiBasePath := "^{appInfo.apiBasePath}" - websiteBasePath := "^{appInfo.websiteBasePath}" + apiBasePath := "/auth" + websiteBasePath := "/auth" err := supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "^{coreInfo.uri}", - APIKey: "^{coreInfo.key}", + ConnectionURI: "", + APIKey: "", }, AppInfo: supertokens.AppInfo{ - AppName: "^{appInfo.appName}", - APIDomain: "^{appInfo.apiDomain}", - WebsiteDomain: "^{appInfo.websiteDomain}", + AppName: "", + APIDomain: "", + WebsiteDomain: "", APIBasePath: &apiBasePath, WebsiteBasePath: &websiteBasePath, }, RecipeList: []supertokens.Recipe{ - // highlight-start passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{ Enabled: true, }, }), - // highlight-end thirdparty.Init(&tpmodels.TypeInput{ /*...*/ }), emailpassword.Init(&epmodels.TypeInput{ /*...*/ }), session.Init(&sessmodels.TypeInput{ @@ -310,11 +295,10 @@ func main() { } } ``` - - - - - + + + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless @@ -322,15 +306,15 @@ from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='fastapi', recipe_list=[ @@ -341,20 +325,16 @@ init( emailpassword.init( # ... ), - # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) - # highlight-end ], mode='asgi' # use wsgi if you are running using gunicorn ) ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless @@ -362,15 +342,15 @@ from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='flask', recipe_list=[ @@ -381,19 +361,15 @@ init( emailpassword.init( # ... ), - # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) - # highlight-end ] ) ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword, session, passwordless @@ -401,15 +377,15 @@ from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig init( app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" ), supertokens_config=SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" + connection_uri="", + api_key="" ), framework='django', recipe_list=[ @@ -420,21 +396,18 @@ init( emailpassword.init( # ... ), - # highlight-start passwordless.init( flow_type="USER_INPUT_CODE", contact_config=ContactPhoneOnlyConfig() ) - # highlight-end ], mode='asgi' # use wsgi if you are running django server in sync mode ) ``` - - - - - + + + + The above exposes all the APIs to the frontend that can be used to create and verify the OTP. @@ -442,61 +415,57 @@ The above exposes all the APIs to the frontend that can be used to create and ve During sign up, once the user has completed the second factor, we want to save their phone number against their profile. For this, we use the `UserMetadata` recipe. -:::important -Make sure to add the User Metadata in the recipe list. +:::note[Make sure to add the User Metadata in the recipe list.] ::: The passwordless recipe creates a new `userId` for the user against which it saves the phone number. We can associate the passwordless `userId` with the `userId` of the first factor, and this way, we associate a phone number to the user: - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Passwordless from "supertokens-node/recipe/passwordless"; Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - override: { - apis: (oI) => { - return { - ...oI, - // this API is called when the user enters the OTP - consumeCodePOST: async function (input) { - // - We should already have a session here since this is called after first factor login - // - We set the claims to check to be [] here, since this needs to be callable - // without the second factor completed - let session = await Session.getSession(input.options.req, input.options.res, { - overrideGlobalClaimValidators: () => [], - }); - - let resp = await oI.consumeCodePOST!(input); - - if (resp.status === "OK") { - // OTP verification was successful. We can now associate - // the passwordless user ID with the thirdpartyemailpassword - // user ID, so that later on, we can fetch the phone number. - await UserMetadata.updateUserMetadata( - session!.getUserId(), // this is the userId of the first factor login - { - passwordlessUserId: resp.user.id, - } - ); - } - - return resp; - }, - }; + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + override: { + apis: (oI) => { + return { + ...oI, + // this API is called when the user enters the OTP + consumeCodePOST: async function (input) { + // - We should already have a session here since this is called after first factor login + // - We set the claims to check to be [] here, since this needs to be callable + // without the second factor completed + let session = await Session.getSession(input.options.req, input.options.res, { + overrideGlobalClaimValidators: () => [], + }); + + let resp = await oI.consumeCodePOST!(input); + + if (resp.status === "OK") { + // OTP verification was successful. We can now associate + // the passwordless user ID with the thirdpartyemailpassword + // user ID, so that later on, we can fetch the phone number. + await UserMetadata.updateUserMetadata( + session!.getUserId(), // this is the userId of the first factor login + { + passwordlessUserId: resp.user.id, + }, + ); + } + + return resp; }, - } -}) + }; + }, + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -557,10 +526,8 @@ func main() { }) } ``` - - - - + + ```python from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions, ConsumeCodePostOkResult from typing import Union, Dict, Any, Optional @@ -624,9 +591,8 @@ passwordless.init( ), ) ``` - - - + + ## 3. Updating the session post second factor auth @@ -634,99 +600,94 @@ We also want to change the session's payload to indicate that the user has compl We also have to be careful about not creating a new session after the second factor auth is completed. By default, the passwordless recipe creates a new session on successful verification, overwriting the older one. We can prevent this, by using the `userContext` feature: - - - -```tsx + + +```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Passwordless from "supertokens-node/recipe/passwordless"; -import { BooleanClaim } from "supertokens-node/recipe/session/claims"; // REMOVE_FROM_OUTPUT -declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - override: { - apis: (oI) => { - return { - ...oI, - // this API is called when the user enters the OTP - consumeCodePOST: async function (input) { - // A session should already exist since this should be called after the first factor is completed. - // We set the claims to check to be [] here, since this needs to be callable - // without the second factor completed - let session = await Session.getSession(input.options.req, input.options.res, { - overrideGlobalClaimValidators: () => [], - }); - - // highlight-start - // we add the existing session to the user context so that the createNewSession - // function doesn't create a new session - input.userContext.session = session; - // highlight-end - - let resp = await oI.consumeCodePOST!(input); - - if (resp.status === "OK") { - // highlight-start - // OTP verification was successful. - // We can now set the SecondFactorClaim in the session to true. - // the user has access to API routes and the frontend UI - await resp.session.setClaimValue(SecondFactorClaim, true); - // highlight-end - - // We can now associate - // the passwordless user ID with the thirdpartyemailpassword - // user ID, so that later on, we can fetch the phone number. - await UserMetadata.updateUserMetadata( - session!.getUserId(), // this is the userId of the first factor login - { - passwordlessUserId: resp.user.id, - } - ); - } - - return resp; - }, - }; + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + override: { + apis: (oI) => { + return { + ...oI, + // this API is called when the user enters the OTP + consumeCodePOST: async function (input) { + // A session should already exist since this should be called after the first factor is completed. + // We set the claims to check to be [] here, since this needs to be callable + // without the second factor completed + let session = await Session.getSession(input.options.req, input.options.res, { + overrideGlobalClaimValidators: () => [], + }); + + // we add the existing session to the user context so that the createNewSession + // function doesn't create a new session + input.userContext.session = session; + + let resp = await oI.consumeCodePOST!(input); + + if (resp.status === "OK") { + // OTP verification was successful. + // We can now set the SecondFactorClaim in the session to true. + // the user has access to API routes and the frontend UI + await resp.session.setClaimValue(SecondFactorClaim, true); + + // We can now associate + // the passwordless user ID with the thirdpartyemailpassword + // user ID, so that later on, we can fetch the phone number. + await UserMetadata.updateUserMetadata( + session!.getUserId(), // this is the userId of the first factor login + { + passwordlessUserId: resp.user.id, + }, + ); + } + + return resp; }, - } -}) + }; + }, + }, +}); Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - /* This function is called after signing in or signing up via the first factor */ - createNewSession: async function (input) { - // highlight-start - if (input.userContext.session !== undefined) { - /** - * This is true for the second factor login. - * So instead of creating a new session, we return the already existing one. - */ - return input.userContext.session; - } - // highlight-end - return originalImplementation.createNewSession({ - ...input, - accessTokenPayload: { - ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), - }, - }); - }, - }; + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + /* This function is called after signing in or signing up via the first factor */ + createNewSession: async function (input) { + if (input.userContext.session !== undefined) { + /** + * This is true for the second factor login. + * So instead of creating a new session, we return the already existing one. + */ + return input.userContext.session; + } + return originalImplementation.createNewSession({ + ...input, + accessTokenPayload: { + ...input.accessTokenPayload, + ...(await SecondFactorClaim.build( + input.userId, + input.recipeUserId, + input.tenantId, + undefined, + input.userContext, + )), + }, + }); }, + }; }, -}) + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -739,11 +700,9 @@ import ( ) func main() { - // highlight-start SecondFactorClaim, _ := claims.BooleanClaim("2fa-completed", func(userId, tenantId string, userContext supertokens.UserContext) (interface{}, error) { return false, nil }, nil) - // highlight-end passwordless.Init(plessmodels.TypeInput{ FlowType: "USER_INPUT_CODE", @@ -768,11 +727,9 @@ func main() { return plessmodels.ConsumeCodePOSTResponse{}, err } - // highlight-start // we add the existing session to the user context so that the createNewSession // function doesn't create a new session (*userContext)["session"] = session - // highlight-end resp, err := oConsumeCodePOST(userInput, linkCode, preAuthSessionID, tenantId, options, userContext) if err != nil { @@ -780,12 +737,10 @@ func main() { } if resp.OK != nil { - // highlight-start // OTP verification was successful. We can now mark the // session's payload as is2faComplete: true so that // the user has access to API routes and the frontend UI resp.OK.Session.SetClaimValue(SecondFactorClaim, true) - // highlight-end // We can now associate // the passwordless user ID with the thirdpartyemailpassword @@ -811,7 +766,6 @@ func main() { oCreateNewSession := *originalImplementation.CreateNewSession /* This function is called after signing in or signing up via the first factor */ (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // highlight-start if session, ok := (*userContext)["session"].(sessmodels.SessionContainer); ok { /** * This will be true for the second factor login. @@ -819,7 +773,6 @@ func main() { */ return session, nil } - // highlight-end if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} @@ -836,10 +789,8 @@ func main() { }) } ``` - - - - + + ```python from supertokens_python.recipe.passwordless.interfaces import ( APIInterface, @@ -888,7 +839,6 @@ def override_passwordless_apis(original_implementation: APIInterface): # we should add the existing session to the user_context # so that the create_new_session function # doesn't create a new session - # highlight-next-line user_context["session"] = _session res = await original_consume_code_post( @@ -904,12 +854,10 @@ def override_passwordless_apis(original_implementation: APIInterface): ) if isinstance(res, ConsumeCodePostOkResult): - # highlight-start # OTP verification was successful. We can now mark the # session's payload as {"is2faComplete": True} so that # the user has access to API routes and the frontend UI await _session.set_claim_value(SecondFactorClaim, True) - # highlight-end # We can now associate # the passwordless user ID with the thirdpartyemailpassword @@ -940,13 +888,11 @@ def override_session_functions(original_implementation: RecipeInterface): # This function is called after signing in or # signing up via the first factor - # highlight-start _session = user_context.get("session") if _session and isinstance(_session, SessionContainer): # This is true for the second factor login. # So instead of creating a new session, we return the already existing one. return _session - # highlight-end if access_token_payload is None: access_token_payload = {} @@ -976,9 +922,8 @@ def override_session_functions(original_implementation: RecipeInterface): original_implementation.create_new_session = create_new_session return original_implementation ``` - - - + + ## 4. Validating the phone number @@ -990,9 +935,8 @@ By default, the Passwordless API for sending an OTP (`createCodePOST`) sends the To make it secure, we override the `createCodePOST` API and check that the input phone number is the same as the phone number associated with the user. If it's not the same, we throw an error, and if it is the same, we continue: - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; @@ -1000,66 +944,64 @@ import Passwordless from "supertokens-node/recipe/passwordless"; import SuperTokens from "supertokens-node"; Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - override: { - apis: (oI) => { - return { - ...oI, - /*This API is called to send an OTP*/ - createCodePOST: async function (input) { - /** - * We want to make sure that the OTP being generated is for the - * same number that belongs to this user. - */ - - // A session should already exist since this should be called after the first factor is completed. - // We set the claims to check to be [] here, since this needs to be callable - // without the second factor completed - let session = await Session.getSession(input.options.req, input.options.res, { - overrideGlobalClaimValidators: () => [], - }); - - // We try and get the phone number associated with this user. It is - // defined if this is a sign in attempt, in which case, we check that - // it is equal to the input phone number - let userMetadata = await UserMetadata.getUserMetadata(session!.getUserId()); - let phoneNumber: string | undefined = undefined; - if (userMetadata.metadata.passwordlessUserId !== undefined) { - // the flow comes here during a login attempt, since we - // associate the passwordless userId to the user on sign up - let passwordlessUserInfo = await SuperTokens.getUser( - userMetadata.metadata.passwordlessUserId as string, - input.userContext, - ); - phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; - } - - if (phoneNumber !== undefined) { - // this means we found a phone number associated to this user. - // we check if the input phone number is the same as this one. - if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { - throw new Error("Input phone number is not the same as the one saved for this user"); - } - } - - return oI.createCodePOST!(input); - }, - consumeCodePOST: async function (input) { - /*...Modifications from previous step */ - let resp = await oI.consumeCodePOST!(input); - /*...Modifications from previous step */ - return resp; - }, - }; + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + override: { + apis: (oI) => { + return { + ...oI, + /*This API is called to send an OTP*/ + createCodePOST: async function (input) { + /** + * We want to make sure that the OTP being generated is for the + * same number that belongs to this user. + */ + + // A session should already exist since this should be called after the first factor is completed. + // We set the claims to check to be [] here, since this needs to be callable + // without the second factor completed + let session = await Session.getSession(input.options.req, input.options.res, { + overrideGlobalClaimValidators: () => [], + }); + + // We try and get the phone number associated with this user. It is + // defined if this is a sign in attempt, in which case, we check that + // it is equal to the input phone number + let userMetadata = await UserMetadata.getUserMetadata(session!.getUserId()); + let phoneNumber: string | undefined = undefined; + if (userMetadata.metadata.passwordlessUserId !== undefined) { + // the flow comes here during a login attempt, since we + // associate the passwordless userId to the user on sign up + let passwordlessUserInfo = await SuperTokens.getUser( + userMetadata.metadata.passwordlessUserId as string, + input.userContext, + ); + phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; + } + + if (phoneNumber !== undefined) { + // this means we found a phone number associated to this user. + // we check if the input phone number is the same as this one. + if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { + throw new Error("Input phone number is not the same as the one saved for this user"); + } + } + + return oI.createCodePOST!(input); }, - } -}) + consumeCodePOST: async function (input) { + /*...Modifications from previous step */ + let resp = await oI.consumeCodePOST!(input); + /*...Modifications from previous step */ + return resp; + }, + }; + }, + }, +}); ``` - - - - + + ```go import ( "errors" @@ -1083,7 +1025,6 @@ func main() { APIs: func(originalImplementation plessmodels.APIInterface) plessmodels.APIInterface { /*This API is called to send an OTP*/ oCreateCodePOST := *originalImplementation.CreateCodePOST - // highlight-start nCreateCodePOST := func(email *string, phoneNumber *string, tenantId string, options plessmodels.APIOptions, userContext supertokens.UserContext) (plessmodels.CreateCodePOSTResponse, error) { /** * We want to make sure that the OTP being generated is for the @@ -1129,7 +1070,6 @@ func main() { return oCreateCodePOST(email, phoneNumber, tenantId, options, userContext) } - // highlight-end *originalImplementation.CreateCodePOST = nCreateCodePOST oConsumeCodePOST := *originalImplementation.ConsumeCodePOST @@ -1147,10 +1087,8 @@ func main() { }) } ``` - - - - + + ```python from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from typing import Union, Dict, Any, Optional @@ -1164,7 +1102,6 @@ def override_passwordless_apis(original_implementation: APIInterface): original_consume_code_post = original_implementation.consume_code_post original_create_code_post = original_implementation.create_code_post - # highlight-start async def create_code_post( email: Union[str, None], phone_number: Union[str, None], @@ -1219,7 +1156,6 @@ def override_passwordless_apis(original_implementation: APIInterface): user_context, ) - # highlight-end async def consume_code_post( pre_auth_session_id: str, @@ -1251,9 +1187,8 @@ def override_passwordless_apis(original_implementation: APIInterface): original_implementation.consume_code_post = consume_code_post return original_implementation ``` - - - + + ## 5. Storing the user's phone number in the session @@ -1261,65 +1196,63 @@ When the session is first created (after the first factor is completed), we stor We do this by modifying the `createNewSession` function in the `Session.init` call: - - - -```tsx + + +```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import SuperTokens from "supertokens-node"; -import { BooleanClaim } from "supertokens-node/recipe/session/claims"; // REMOVE_FROM_OUTPUT -declare const SecondFactorClaim: BooleanClaim; // REMOVE_FROM_OUTPUT Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - /* This function is called after signing in or signing up via the first factor */ - createNewSession: async function (input) { - if (input.userContext.session !== undefined) { - /** - * This is true for the second factor login. - * So instead of creating a new session, we return the already existing one. - */ - return input.userContext.session; - } - - // highlight-start - // we first get the passwordless userId associated with this user - // using the UserMetadata recipe - let userMetadata = await UserMetadata.getUserMetadata(input.userId); - let phoneNumber: string | undefined = undefined; - if (userMetadata.metadata.passwordlessUserId !== undefined) { - // We get the phone number associated with the passwordless userId. - let passwordlessUserInfo = await SuperTokens.getUser( - userMetadata.metadata.passwordlessUserId as string, - input.userContext, - ); - phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; - } - // highlight-end - - return originalImplementation.createNewSession({ - ...input, - accessTokenPayload: { - ...input.accessTokenPayload, - ...(await SecondFactorClaim.build(input.userId, input.recipeUserId, input.tenantId, undefined, input.userContext)), - // highlight-next-line - phoneNumber, - }, - }); - }, - }; + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + /* This function is called after signing in or signing up via the first factor */ + createNewSession: async function (input) { + if (input.userContext.session !== undefined) { + /** + * This is true for the second factor login. + * So instead of creating a new session, we return the already existing one. + */ + return input.userContext.session; + } + + // we first get the passwordless userId associated with this user + // using the UserMetadata recipe + let userMetadata = await UserMetadata.getUserMetadata(input.userId); + let phoneNumber: string | undefined = undefined; + if (userMetadata.metadata.passwordlessUserId !== undefined) { + // We get the phone number associated with the passwordless userId. + let passwordlessUserInfo = await SuperTokens.getUser( + userMetadata.metadata.passwordlessUserId as string, + input.userContext, + ); + phoneNumber = passwordlessUserInfo?.phoneNumbers[0]; + } + + return originalImplementation.createNewSession({ + ...input, + accessTokenPayload: { + ...input.accessTokenPayload, + ...(await SecondFactorClaim.build( + input.userId, + input.recipeUserId, + input.tenantId, + undefined, + input.userContext, + )), + phoneNumber, + }, + }); }, + }; }, -}) + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -1349,7 +1282,6 @@ func main() { return session, nil } - // highlight-start // we first get the passwordless userId associated with this user // using the UserMetadata recipe userMetadata, err := usermetadata.GetUserMetadata(userID, userContext) @@ -1364,7 +1296,6 @@ func main() { } userPhoneNumber = passwordlessUserInfo.PhoneNumber } - // highlight-end if accessTokenPayload == nil { accessTokenPayload = map[string]interface{}{} @@ -1373,11 +1304,9 @@ func main() { if err != nil { return nil, err } - // highlight-start if userPhoneNumber != nil { accessTokenPayload["phoneNumber"] = *userPhoneNumber } - // highlight-end return oCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) } return originalImplementation @@ -1386,10 +1315,8 @@ func main() { }) } ``` - - - - + + ```python from typing import Dict, Any, Optional from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata @@ -1430,7 +1357,6 @@ def override_session_functions(original_implementation: RecipeInterface): if access_token_payload is None: access_token_payload = {} - # highlight-start # we first get the passwordless user id associated with this user # using the user_metadata recipe user_metadata = await get_user_metadata(user_id) @@ -1442,7 +1368,6 @@ def override_session_functions(original_implementation: RecipeInterface): ) if passwordless_user_info is not None: phone_number = passwordless_user_info.phone_numbers[0] - # highlight-end # Insert "is2faComplete" and "phoneNumber" in the access token payload access_token_payload = { @@ -1456,7 +1381,6 @@ def override_session_functions(original_implementation: RecipeInterface): user_context, ) ), - # highlight-next-line "phoneNumber": phone_number, } return await original_create_new_session( @@ -1472,68 +1396,63 @@ def override_session_functions(original_implementation: RecipeInterface): original_implementation.create_new_session = create_new_session return original_implementation ``` - - - + + We can then further modify the customisation in step (4) to simply read from the session's payload making it more efficient: - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; import Passwordless from "supertokens-node/recipe/passwordless"; Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - override: { - apis: (oI) => { - return { - ...oI, - /*This API is called to send an OTP*/ - createCodePOST: async function (input) { - /** - * We want to make sure that the OTP being generated is for the - * same number that belongs to this user. - */ - - // A session should already exist since this should be called after the first factor is completed. - // We remove claim checking here, since this needs to be callable without the second factor completed - let session = await Session.getSession(input.options.req, input.options.res, { - overrideGlobalClaimValidators: () => [], - }); - - // highlight-next-line - let phoneNumber: string = session!.getAccessTokenPayload().phoneNumber; - - if (phoneNumber !== undefined) { - // this means we found a phone number associated to this user. - // we check if the input phone number is the same as this one. - if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { - throw new Error("Input phone number is not the same as the one saved for this user"); - } - } - - return oI.createCodePOST!(input); - }, - consumeCodePOST: async function (input) { - /*...Modifications from previous step */ - let resp = await oI.consumeCodePOST!(input); - /*...Modifications from previous step */ - return resp; - }, - }; + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + override: { + apis: (oI) => { + return { + ...oI, + /*This API is called to send an OTP*/ + createCodePOST: async function (input) { + /** + * We want to make sure that the OTP being generated is for the + * same number that belongs to this user. + */ + + // A session should already exist since this should be called after the first factor is completed. + // We remove claim checking here, since this needs to be callable without the second factor completed + let session = await Session.getSession(input.options.req, input.options.res, { + overrideGlobalClaimValidators: () => [], + }); + + let phoneNumber: string = session!.getAccessTokenPayload().phoneNumber; + + if (phoneNumber !== undefined) { + // this means we found a phone number associated to this user. + // we check if the input phone number is the same as this one. + if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) { + throw new Error("Input phone number is not the same as the one saved for this user"); + } + } + + return oI.createCodePOST!(input); + }, + consumeCodePOST: async function (input) { + /*...Modifications from previous step */ + let resp = await oI.consumeCodePOST!(input); + /*...Modifications from previous step */ + return resp; }, - } -}) + }; + }, + }, +}); ``` - - - - + + ```go import ( "errors" @@ -1574,12 +1493,10 @@ func main() { return plessmodels.CreateCodePOSTResponse{}, err } - // highlight-start var userPhoneNumber *string if phoneNumber, ok := session.GetAccessTokenPayloadWithContext(userContext)["phoneNumber"].(string); ok { userPhoneNumber = &phoneNumber } - // highlight-end if userPhoneNumber != nil { // this means we found a phone number associated to this user. @@ -1608,10 +1525,8 @@ func main() { }) } ``` - - - - + + ```python from supertokens_python.recipe.passwordless.interfaces import APIInterface, APIOptions from typing import Union, Dict, Any, Optional @@ -1644,7 +1559,6 @@ def override_passwordless_apis(original_implementation: APIInterface): ) assert _session is not None - # highlight-next-line payload_phone_number = _session.get_access_token_payload().get("phoneNumber") if payload_phone_number is not None: @@ -1667,6 +1581,5 @@ def override_passwordless_apis(original_implementation: APIInterface): original_implementation.create_code_post = create_code_post ``` - - - + + diff --git a/docs/additional-verification/mfa/legacy-mfa/frontend-custom.mdx b/docs/additional-verification/mfa/legacy-mfa/frontend-custom.mdx index c8ab048221..6a47e87cb6 100644 --- a/docs/additional-verification/mfa/legacy-mfa/frontend-custom.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/frontend-custom.mdx @@ -1,141 +1,92 @@ --- -id: frontend-custom title: Using a custom UI -hide_title: true -sidebar_position: 4 -description: >- - Implement custom UI for multi-factor authentication using SuperTokens recipes - and session management. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement custom UI for multi-factor authentication using SuperTokens recipes and session management. +sidebar: + hidden: true + order: 4 --- - -# Using a custom UI - ## 1. First factor recipe init Start by following the recipe guide for first factor login. To continue building our example app, we will use the `thirdparty` and the `emailpassword` recipes as the first factor. -After following the [frontend quick setup section](/docs/quickstart/introduction) and the [social login guide](/docs/authentication/social/initial-setup), you should have the following `supertokens.init`: - - - - - +After following the [frontend quick setup section](/quickstart) and the [social login guide](/authentication/social/initial-setup), you should have the following `supertokens.init`: + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; -import ThirdParty from 'supertokens-web-js/recipe/thirdparty'; +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; +import ThirdParty from "supertokens-web-js/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - Session.init(), - EmailPassword.init(), - ThirdParty.init() - ], + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [Session.init(), EmailPassword.init(), ThirdParty.init()], }); ``` - - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - supertokensEmailPassword.init(), - supertokensThirdParty.init() - ], + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [supertokensSession.init(), supertokensEmailPassword.init(), supertokensThirdParty.init()], }); ``` - - - + + From here on, you can continue to build out the first factor's login form using the functions exposed from the `supertokens-web-js` SDK. ## 2. Second factor recipe init -For the second factor, we will be using the [passwordless recipe](https://supertokens.com/docs/passwordless/introduction). After following the [frontend quick setup section](https://supertokens.com/docs/passwordless/quick-setup/frontend), you should have the following `supertokens.init`: - - - - - +For the second factor, we will be using the [passwordless recipe](https://supertokens.com/docs/authentication/passwordless/initial-setup). After following the [frontend quick setup section](https://supertokens.com/docs/authentication/passwordless/initial-setup), you should have the following `supertokens.init`: + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; -import ThirdParty from 'supertokens-web-js/recipe/thirdparty'; -// highlight-next-line -import Passwordless from 'supertokens-web-js/recipe/passwordless'; +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; +import ThirdParty from "supertokens-web-js/recipe/thirdparty"; +import Passwordless from "supertokens-web-js/recipe/passwordless"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - Session.init(), - EmailPassword.init(), - ThirdParty.init(), - // highlight-next-line - Passwordless.init(), - ], + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [Session.init(), EmailPassword.init(), ThirdParty.init(), Passwordless.init()], }); ``` - - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - supertokensEmailPassword.init(), - supertokensThirdParty.init(), - // highlight-next-line - supertokensPasswordless.init() - ], + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [ + supertokensSession.init(), + supertokensEmailPassword.init(), + supertokensThirdParty.init(), + supertokensPasswordless.init(), + ], }); ``` - - - + + You can use the passwordless recipe function to build the second factor UI. @@ -145,94 +96,80 @@ You will want to handle the routing of `webapp` to make sure that the correct lo ### Checking if the first factor login should be shown - - - + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function shouldShowFirstFactor() { - return !(await Session.doesSessionExist()); + return !(await Session.doesSessionExist()); } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldShowFirstFactor() { - return !(await supertokensSession.doesSessionExist()); + return !(await supertokensSession.doesSessionExist()); } ``` - - - + + If a session does not exist, this means that the user has not completed the first factor. In this case, you want to route them to the `thirdparty` + `emailpassword` login screen. ### Checking if the second factor login should be shown - - - + + ```tsx -import Session, { BooleanClaim } from 'supertokens-web-js/recipe/session'; +import Session, { BooleanClaim } from "supertokens-web-js/recipe/session"; -// highlight-start export const SecondFactorClaim = new BooleanClaim({ - id: "2fa-completed", - refresh: async () => { - // no-op - }, + id: "2fa-completed", + refresh: async () => { + // no-op + }, }); async function shouldShowSecondFactor() { - if(await shouldShowFirstFactor()) { - return false; - } + if (await shouldShowFirstFactor()) { + return false; + } - return !(await Session.getClaimValue({ claim: SecondFactorClaim })); + return !(await Session.getClaimValue({ claim: SecondFactorClaim })); } -// highlight-end async function shouldShowFirstFactor() { - return !(await Session.doesSessionExist()); + return !(await Session.doesSessionExist()); } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -// highlight-start + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // This could be moved into a separate file... export const SecondFactorClaim = new supertokensSession.BooleanClaim({ - id: "2fa-completed", - refresh: async () => { - // This is something we have no way of refreshing, so this is a no-op - }, + id: "2fa-completed", + refresh: async () => { + // This is something we have no way of refreshing, so this is a no-op + }, }); async function shouldShowSecondFactor() { - if(await shouldShowFirstFactor()) { - return false; - } - if (await supertokensSession.getClaimValue({ claim: SecondFactorClaim })) { - return false; - } - return true; + if (await shouldShowFirstFactor()) { + return false; + } + if (await supertokensSession.getClaimValue({ claim: SecondFactorClaim })) { + return false; + } + return true; } -// highlight-end async function shouldShowFirstFactor() { - return !(await supertokensSession.doesSessionExist()); + return !(await supertokensSession.doesSessionExist()); } ``` - - - + + - If a session does not exist, it means that the user has not finished the first factor yet. - If a session exists, but the `SecondFactorClaim` value is `true`, it means that the user has finished both the factors. @@ -241,158 +178,138 @@ async function shouldShowFirstFactor() { ### Protecting a website route that requires both the factors You can check if a user has finished both the login factors using the two functions above: -```tsx +```tsx check=false reason="walkthrough excerpt relies on helper functions or values defined in surrounding steps" async function areBothLoginFactorsCompleted(): Promise { - // @ts-ignore - return !(await shouldShowFirstFactor()) && !(await shouldShowSecondFactor()) + return !(await shouldShowFirstFactor()) && !(await shouldShowSecondFactor()); } areBothLoginFactorsCompleted().then(async (bothFactorsCompleted) => { - if (bothFactorsCompleted) { - // update state to show UI + if (bothFactorsCompleted) { + // update state to show UI + } else { + if (await shouldShowFirstFactor()) { + // redirect user to first factor } else { - // @ts-ignore - if (await shouldShowFirstFactor()) { - // redirect user to first factor - } else { - // redirect user to second factor screen - } + // redirect user to second factor screen } -}) + } +}); ``` ## 4. Getting the user's phone number for the second factor Once the user has finished the sign up process, we save their phone number in the session (as seen in the backend setup steps). This can be accessed on the frontend to send the OTP to the user without asking them to re-enter their phone after sign in: - - - + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function getUsersPhoneNumber(): Promise { - if (!(await Session.doesSessionExist())) { - // the user has not finished the first factor. - return undefined; - } - let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); - if (accessTokenPayload.phoneNumber === undefined) { - // this means that the user is still signing up, or it means that the user - // had previously tried to sign up, but didn't complete the second factor step, - // and has now just signed in. - - // In this case, we should ask the user to enter their phone number. - return undefined; - } - - // An OTP can be sent to this phone for the second factor. - // No need to ask the user to enter their phone number again. - return accessTokenPayload.phoneNumber; + if (!(await Session.doesSessionExist())) { + // the user has not finished the first factor. + return undefined; + } + let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); + if (accessTokenPayload.phoneNumber === undefined) { + // this means that the user is still signing up, or it means that the user + // had previously tried to sign up, but didn't complete the second factor step, + // and has now just signed in. + + // In this case, we should ask the user to enter their phone number. + return undefined; + } + + // An OTP can be sent to this phone for the second factor. + // No need to ask the user to enter their phone number again. + return accessTokenPayload.phoneNumber; } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function getUsersPhoneNumber(): Promise { - if (!(await supertokensSession.doesSessionExist())) { - // the user has not finished the first factor. - return undefined; - } - let accessTokenPayload = await supertokensSession.getAccessTokenPayloadSecurely(); - if (accessTokenPayload.phoneNumber === undefined) { - // this means that the user is still signing up, or it means that the user - // had previously tried to sign up, but didn't complete the second factor step, - // and has now just signed in. - - // In this case, we should ask the user to enter their phone number. - return undefined; - } - - // An OTP can be sent to this phone for the second factor. - // No need to ask the user to enter their phone number again. - return accessTokenPayload.phoneNumber; + if (!(await supertokensSession.doesSessionExist())) { + // the user has not finished the first factor. + return undefined; + } + let accessTokenPayload = await supertokensSession.getAccessTokenPayloadSecurely(); + if (accessTokenPayload.phoneNumber === undefined) { + // this means that the user is still signing up, or it means that the user + // had previously tried to sign up, but didn't complete the second factor step, + // and has now just signed in. + + // In this case, we should ask the user to enter their phone number. + return undefined; + } + + // An OTP can be sent to this phone for the second factor. + // No need to ask the user to enter their phone number again. + return accessTokenPayload.phoneNumber; } ``` - - - + + ## 5. Implementing logout If the user has completed both the factors, implementing the sign out feature can be done by: - - - + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function signOut() { - await Session.signOut(); - // redirect the user to the first factor login screen + await Session.signOut(); + // redirect the user to the first factor login screen } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function signOut() { - await supertokensSession.signOut(); - // redirect the user to the first factor login screen + await supertokensSession.signOut(); + // redirect the user to the first factor login screen } ``` - - - + + You should also implement a sign out button on the second factor screen, otherwise the user would be in a stuck state if they are unable to complete the second factor. To do this, you will need to call the `signOut` function as well as a function to clear the passwordless login state: - - - -```tsx -import Session from 'supertokens-web-js/recipe/session'; -import Passwordless from 'supertokens-web-js/recipe/passwordless'; + + +```tsx check=false reason="walkthrough excerpt relies on helper functions or values defined in surrounding steps" +import Session from "supertokens-web-js/recipe/session"; +import Passwordless from "supertokens-web-js/recipe/passwordless"; async function signOut() { - // @ts-ignore - if (await shouldShowSecondFactor()) { - // this means we are on the second factor screen now. - // calling the function below clears the login attempt info that is - // saved on the browser during passwordless login. This is needed so that - // future login attempts are not affected by the current one. - await Passwordless.clearLoginAttemptInfo(); - } - await Session.signOut(); - // redirect the user to the first factor login screen + if (await shouldShowSecondFactor()) { + // this means we are on the second factor screen now. + // calling the function below clears the login attempt info that is + // saved on the browser during passwordless login. This is needed so that + // future login attempts are not affected by the current one. + await Passwordless.clearLoginAttemptInfo(); + } + await Session.signOut(); + // redirect the user to the first factor login screen } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function signOut() { - // @ts-ignore - if (await shouldShowSecondFactor()) { - // this means we are on the second factor screen now. - // calling the function below clears the login attempt info that is - // saved on the browser during passwordless login. This is needed so that - // future login attempts are not affected by the current one. - await supertokensPasswordless.clearLoginAttemptInfo(); - } - await supertokensSession.signOut(); - // redirect the user to the first factor login screen + if (await shouldShowSecondFactor()) { + // this means we are on the second factor screen now. + // calling the function below clears the login attempt info that is + // saved on the browser during passwordless login. This is needed so that + // future login attempts are not affected by the current one. + await supertokensPasswordless.clearLoginAttemptInfo(); + } + await supertokensSession.signOut(); + // redirect the user to the first factor login screen } ``` - - - + + diff --git a/docs/additional-verification/mfa/legacy-mfa/how-it-works.mdx b/docs/additional-verification/mfa/legacy-mfa/how-it-works.mdx index b6cabf9085..8d2649d026 100644 --- a/docs/additional-verification/mfa/legacy-mfa/how-it-works.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/how-it-works.mdx @@ -1,25 +1,21 @@ --- -id: how-it-works title: How it works -hide_title: true -sidebar_position: 2 description: >- - Implement multi-factor authentication using customizable auth recipes, - including session management and second factor validation. -page_type: guide -recipe: mfa -category: multi-factor-authentication + Implement multi-factor authentication using customizable auth recipes, including session management and second factor + validation. +sidebar: + hidden: true + order: 2 --- -:::info Caution +:::info[Caution] This is the legacy method of implementing MFA. It has multiple [disadvantages](./legacy-vs-new) compared to using our MFA recipe. ::: -# How it works -You need to start by choosing your first factor auth. This can be any of the [auth recipes](https://supertokens.com/docs/guides) we support. A common choice is to combine the `thirdparty` and `emailpassword` recipes, which allows users to sign in with social or email / password login. +You need to start by choosing your first factor auth. This can be any of the [auth recipes](https://supertokens.com/docs/authentication/overview) we support. A common choice is to combine the `thirdparty` and `emailpassword` recipes, which allows users to sign in with social or email / password login. -For the second factor, whilst you can choose any of our auth recipes as well, the most common choice is the [Passwordless recipe](https://supertokens.com/docs/passwordless/introduction), using which, you can send SMS or email OTP (or magic links) to the user. +For the second factor, whilst you can choose any of our auth recipes as well, the most common choice is the [Passwordless recipe](https://supertokens.com/docs/authentication/passwordless/initial-setup), using which, you can send SMS or email OTP (or magic links) to the user. You will also need to use our Session recipe which can be used to store information about which factors have been completed by the user for the current session. This will be integral to our implementation. @@ -30,7 +26,7 @@ As a high level flow, we will customise these recipes in the following way: - Similarly, to protect frontend routes, we will add the `SecondFactorClaim` validator to the global validators ensuring that all components wrapped with the `SessionAuth` component will check that 2FA is completed. If not, we can then reroute the user to the second factor screen. - We also need to use the `UserMetadata` recipe to store information about the second factor authentication's identification. In the example app, we use phone number SMS OTP as the second factor, therefore we store the user's phone number using the `UserMetadata` recipe, and only send OTPs to that number during sign in. The phone number itself is obtained during the sign up flow. -:::info Important +:::info[Important] In the subsequent sections, we will see how to implement 2fa with the first factor being social and email / password login, and the second factor being phone SMS OTP. If you require a different set of factors or behaviour, you can take inspiration from this guide. diff --git a/docs/additional-verification/mfa/legacy-mfa/legacy-vs-new.mdx b/docs/additional-verification/mfa/legacy-mfa/legacy-vs-new.mdx index 3ea09580c9..45ab4c539c 100644 --- a/docs/additional-verification/mfa/legacy-mfa/legacy-vs-new.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/legacy-vs-new.mdx @@ -1,18 +1,11 @@ --- -id: legacy-vs-new title: Legacy vs New method -hide_title: true -sidebar_position: 1 -description: >- - Compare legacy and new MFA methods, highlighting limitations and scenarios for - using the legacy approach. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Compare legacy and new MFA methods, highlighting limitations and scenarios for using the legacy approach. +sidebar: + hidden: true + order: 1 --- -# Legacy vs New method - The legacy method for MFA requires you to customise the auth recipes we have to add MFA on top of it. Since it does not use the MFA recipe we have, it's free. The limitations of using the legacy method are: diff --git a/docs/additional-verification/mfa/legacy-mfa/meta.ts b/docs/additional-verification/mfa/legacy-mfa/meta.ts new file mode 100644 index 0000000000..247e55738b --- /dev/null +++ b/docs/additional-verification/mfa/legacy-mfa/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Legacy method", + icon: "history", + order: 130, +}); diff --git a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/_category_.json b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/_category_.json deleted file mode 100644 index b2f67741f7..0000000000 --- a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Using prebuilt UI", - "position": 5 -} diff --git a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/init.mdx b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/init.mdx index f349e545bd..9da02f4386 100644 --- a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/init.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/init.mdx @@ -1,60 +1,52 @@ --- -id: init title: 1. Recipe init -hide_title: true -sidebar_position: 1 -description: >- - Initialize authentication with ThirdParty, EmailPassword, Passwordless, and - MultiFactorAuth using SuperTokens. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Initialize authentication with ThirdParty, EmailPassword, Passwordless, and MultiFactorAuth using SuperTokens. +sidebar: + hidden: true + order: 1 --- - -:::info Caution +:::info[Caution] This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: -# 1. Recipe init -To start, we want to initialise the [ThirdParty + EmailPassword](https://supertokens.com/docs/thirdpartyemailpassword/quick-setup/frontend), the [Passwordless](https://supertokens.com/docs/passwordless/quick-setup/frontend) and the MFA recipes: +To start, we want to initialise the [ThirdParty + EmailPassword](https://supertokens.com/docs/authentication/social/initial-setup), the [Passwordless](https://supertokens.com/docs/authentication/passwordless/initial-setup) and the MFA recipes: - ```tsx -import React from 'react'; +import React from "react"; import SuperTokens from "supertokens-auth-react"; -import EmailPassword from 'supertokens-auth-react/recipe/emailpassword'; -import ThirdParty from 'supertokens-auth-react/recipe/thirdparty'; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; SuperTokens.init({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // first factor method - EmailPassword.init(), - ThirdParty.init({ - // ... - }), - // second factor method - Passwordless.init({ - contactMethod: "PHONE" - }), - Session.init(), - MultiFactorAuth.init({ - firstFactors: ["emailpassword", "thirdparty"], - }), - ] + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + // first factor method + EmailPassword.init(), + ThirdParty.init({ + // ... + }), + // second factor method + Passwordless.init({ + contactMethod: "PHONE", + }), + Session.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }), + ], }); ``` @@ -65,6 +57,6 @@ In the subsequent sections, we will be seeing how to modify theses `init` calls - Auto skipping the screen which asks the user to input their phone number if we already have it - post sign in. - Implementing a logout button on the second factor pre-built UI screen. -:::info Important +:::info[Important] In the guide, we will assume that the first factor path is `/auth`, and the second factor path is `/second-factor`. ::: diff --git a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/meta.ts b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/meta.ts new file mode 100644 index 0000000000..f8db875a20 --- /dev/null +++ b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Using prebuilt UI", + icon: "palette", + order: 5, +}); diff --git a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/protecting-routes.mdx b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/protecting-routes.mdx index 294e80697a..5269e695e1 100644 --- a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/protecting-routes.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/protecting-routes.mdx @@ -1,73 +1,64 @@ --- -id: protecting-routes title: 3. Protecting routes -hide_title: true -sidebar_position: 3 -description: >- - Wrap application routes with `SessionAuth` to ensure multi-factor - authentication completion. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Wrap application routes with `SessionAuth` to ensure multi-factor authentication completion. +sidebar: + hidden: true + order: 3 --- -:::info Caution +:::info[Caution] This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: -# 3. Protecting routes Now we can wrap your application routes with the `SessionAuth` component, which should check for MFA completion by default: -```tsx +```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; -import { - Routes, - Route, -} from "react-router-dom"; +import { Routes, Route } from "react-router-dom"; import * as reactRouterDom from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -// @ts-ignore import SecondFactor from "./SecondFactor"; -// @ts-ignore -import Home from "./Home" +import Home from "./Home"; function App() { - return ( - -
-
- - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI, EmailPasswordPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-start - - - - } - /> - // highlight-end - - - - } - /> - -
-
-
- ); + return ( + +
+
+ + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + ThirdPartyPreBuiltUI, + EmailPasswordPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} + + + + } + /> + + + + } + /> + +
+
+
+ ); } ``` - diff --git a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/showing-login-ui.mdx b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/showing-login-ui.mdx index bd7f2af1f5..7280c653d4 100644 --- a/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/showing-login-ui.mdx +++ b/docs/additional-verification/mfa/legacy-mfa/prebuilt-ui/showing-login-ui.mdx @@ -1,26 +1,19 @@ --- -id: showing-login-ui title: 2. Showing the first and second factor UI -hide_title: true -sidebar_position: 2 description: Implement a custom second factor UI for legacy multi-factor authentication. -page_type: guide -recipe: mfa -category: multi-factor-authentication +sidebar: + hidden: true + order: 2 --- - -:::info Caution +:::info[Caution] This is the legacy method of implementing MFA. It has multiple [disadvantages](../legacy-vs-new) compared to using our MFA recipe. ::: -# 2. Showing the first and second factor UI - - ## First factor UI -You should see the third party + email password login UI when you visit `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}`. +You should see the third party + email password login UI when you visit `/auth`. No further step is required for the first factor. @@ -33,55 +26,50 @@ Create a file called `SecondFactorClaim.tsx` in which you can add the following import { BooleanClaim } from "supertokens-auth-react/recipe/session"; const SecondFactorClaim = new BooleanClaim({ - id: "2fa-completed", - refresh: async () => { - // This is something we have no way of refreshing, so this is a no-op - }, - onFailureRedirection: () => "/second-factor", + id: "2fa-completed", + refresh: async () => { + // This is something we have no way of refreshing, so this is a no-op + }, + onFailureRedirection: () => "/second-factor", }); -export default SecondFactorClaim +export default SecondFactorClaim; ``` Then in the main `session.init` in the `supertokens.init` block, add this claim's validator to run the check on each route. -```tsx -import React from 'react'; +```tsx check=false reason="application example imports local modules defined elsewhere" +import React from "react"; import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; -// @ts-ignore import SecondFactorClaim from "./SecondFactorClaim"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; SuperTokens.init({ - appInfo: { - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "/auth", - websiteBasePath: "/auth" - }, - recipeList: [ - // other recipes.. - Session.init({ - override: { - functions: (oI) => ({ - ...oI, - // highlight-start - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => { - return [ - SecondFactorClaim.validators.isTrue(), - ...claimValidatorsAddedByOtherRecipes.filter( - (v) => v.id !== MultiFactorAuth.MultiFactorAuthClaim.id - ), - ]; - }, - // highlight-end - }), - }, - }) - ] + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + // other recipes.. + Session.init({ + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => { + return [ + SecondFactorClaim.validators.isTrue(), + ...claimValidatorsAddedByOtherRecipes.filter((v) => v.id !== MultiFactorAuth.MultiFactorAuthClaim.id), + ]; + }, + }), + }, + }), + ], }); ``` @@ -112,45 +100,47 @@ import { PasswordlessComponentsOverrideProvider } from "supertokens-auth-react/r import { useSessionContext } from "supertokens-auth-react/recipe/session"; import { AuthRecipeComponentsOverrideContextProvider } from "supertokens-auth-react/ui"; - function App() { - return ( - - { - if (props.factorIds.includes("otp-phone")) { -
- Second factor auth -
; - } - return ; - }, - }}> - { - const session = useSessionContext(); - - if (session.loading !== true && session.accessTokenPayload.phoneNumber === undefined) { - // this will show the change phone number button - return ; - } - - // this will hide the change phone number button - return null; - }, - }}> - {/* Rest of the JSX */} - -
-
- ); + return ( + + { + if (props.factorIds.includes("otp-phone")) { +
+ Second factor auth +
; + } + return ; + }, + }} + > + { + const session = useSessionContext(); + + if (session.loading !== true && session.accessTokenPayload.phoneNumber === undefined) { + // this will show the change phone number button + return ; + } + + // this will hide the change phone number button + return null; + }, + }} + > + {/* Rest of the JSX */} + +
+
+ ); } export default App; ``` @@ -159,44 +149,43 @@ export default App; Finally, we add the custom component we copy / pasted before to our router: -```tsx -import { - Routes, - Route, -} from "react-router-dom"; +```tsx check=false reason="application example imports local modules defined elsewhere" +import { Routes, Route } from "react-router-dom"; import * as reactRouterDom from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui" +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; -import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui" -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui" +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; -// @ts-ignore import SecondFactor from "./SecondFactor"; function App() { - return ( - -
-
- - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI, EmailPasswordPreBuiltUI, PasswordlessPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-start - - - - } - /> - // highlight-end - -
-
-
- ); + return ( + +
+
+ + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + ThirdPartyPreBuiltUI, + EmailPasswordPreBuiltUI, + PasswordlessPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} + + + + } + /> + +
+
+
+ ); } ``` diff --git a/docs/additional-verification/mfa/meta.ts b/docs/additional-verification/mfa/meta.ts new file mode 100644 index 0000000000..8cf1b7f19a --- /dev/null +++ b/docs/additional-verification/mfa/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Multi Factor Authentication", + icon: "monitor-smartphone", + order: 1.2, +}); diff --git a/docs/additional-verification/mfa/migration/_category_.json b/docs/additional-verification/mfa/migration/_category_.json deleted file mode 100644 index 6481579300..0000000000 --- a/docs/additional-verification/mfa/migration/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Migration", - "position": 9 -} diff --git a/docs/additional-verification/mfa/migration/legacy-to-new.mdx b/docs/additional-verification/mfa/migration/legacy-to-new.mdx index 3209584161..10b78321c5 100644 --- a/docs/additional-verification/mfa/migration/legacy-to-new.mdx +++ b/docs/additional-verification/mfa/migration/legacy-to-new.mdx @@ -1,21 +1,12 @@ --- -id: legacy-to-new title: Migration from legacy MFA to new MFA method -hide_title: true -sidebar_position: 2 -description: >- - Migrate from legacy to new MFA method, integrating backend and frontend - changes for enhanced security. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Migrate from legacy to new MFA method, integrating backend and frontend changes for enhanced security. +sidebar: + order: 2 + hidden: true --- - - -# Migration from legacy MFA to new MFA method - -This page only applies if you have used our [legacy MFA method](/docs/additional-verification/mfa/legacy-mfa/how-it-works) and want to migrate to the new MFA method. +This page only applies if you have used our [legacy MFA method](/additional-verification/mfa/legacy-mfa/how-it-works) and want to migrate to the new MFA method. From a code point of view, [here is the diff](https://github.com/supertokens/supertokens-auth-react/commit/f96e11a527ccd9df71c25798c316dd3de770dde1) of all changes required: - We no longer need the custom `secondFactoeClaim` since the MFA recipe adds its own MFA claim. diff --git a/docs/additional-verification/mfa/migration/old-sdk-to-new.mdx b/docs/additional-verification/mfa/migration/old-sdk-to-new.mdx index 31d4d2a55e..f7574e9431 100644 --- a/docs/additional-verification/mfa/migration/old-sdk-to-new.mdx +++ b/docs/additional-verification/mfa/migration/old-sdk-to-new.mdx @@ -1,110 +1,115 @@ --- -id: old-sdk-to-new title: Migration from an older SuperTokens SDK to a newer one -hide_title: true -sidebar_position: 3 -description: >- - Upgrade your SuperTokens SDK to enable MFA, ensuring compatibility and smooth - transition for existing users. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Upgrade your SuperTokens SDK to enable MFA, ensuring compatibility and smooth transition for existing users. +sidebar: + order: 3 + hidden: true --- - -# Migration from an older SuperTokens SDK to a newer one - This section is applicable to those who want to enable MFA for the first time and are already using SuperTokens in production. The following are the steps you need to take: -- Make sure that you have updated your SuperTokens core, backend SDK and frontend SDK that supports MFA (you can find these versions in the CHANGELOG.md files in their respective GitHub repository). Make sure to read the migration guide for each of the breaking version upgrades. You should aim to get your existing feature set working with the new version of SuperTokens before you enable MFA. +- Make sure that you have updated your SuperTokens core, backend SDK and frontend SDK that supports MFA (you can find these versions in the CHANGELOG files in their respective GitHub repository). Make sure to read the migration guide for each of the breaking version upgrades. You should aim to get your existing feature set working with the new version of SuperTokens before you enable MFA. - Follow the backend and frontend setup we have in this guide along with the factor specific setup (TOTP or email / SMS OTP). ## If enabling MFA for all users at once -If you have enabled MFA for all users, then existing logged in users will asked to complete the secondary factor as soon as they visit your app / website once you have pushed the changes to production. This happens because their existing session is modified to add the MFA claim into it, however, the `v` value in the claim will be `false` since they have not completed MFA yet. This would fail the validators on the frontend which would redirect the user to the MFA login screen. +If you have enabled MFA for all users, then existing logged in users will be asked to complete the secondary factor as soon as they visit your app / website once you have pushed the changes to production. This happens because their existing session is modified to add the MFA claim into it, however, the `v` value in the claim will be `false` since they have not completed MFA yet. This would fail the validators on the frontend which would redirect the user to the MFA login screen. -If you have clients like mobile apps, which take time to upgrade across your entire user base, then you may want to remove the global MFA validator on the backend and make it run only when you know that the request is coming from an updated client: +If you have clients like mobile apps, which take time to upgrade across your entire user base, you can temporarily exempt a server-managed migration cohort from the global MFA validator: - - +The application helper used below must look up eligibility in application-owned server-side storage. Bind each record to the authenticated user and session or to a registered device. Return `false` if the identity or binding is missing, invalid, expired, or outside the cohort. Also return `false` for sensitive operations, such as account recovery, credential changes, payments, or administrative actions, so that they always require MFA step-up. +You may use the exact legacy `client-version` value (`1.0` in this example) only as one input when enrolling the user, session, or device into the server-side cohort. Enrollment must also satisfy server-verified migration policy; the client-controlled header must not create an eligibility record by itself or authorize an exemption at request time. Missing, malformed, and unknown values must not create an enrollment. Set an expiry on every record and a migration sunset date when you will require version `2.0` and remove the exception. + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + ```tsx import Session from "supertokens-node/recipe/session"; import { ClaimValidationResult } from "supertokens-node/recipe/session/types"; -import SuperTokens, { getRequestFromUserContext } from "supertokens-node"; +import SuperTokens, { RecipeUserId } from "supertokens-node"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { UserContext } from "supertokens-node/types" +import { UserContext } from "supertokens-node/types"; + +async function isMfaMigrationExemptionEligible(input: { + tenantId: string; + userId: string; + recipeUserId: RecipeUserId; + userContext: UserContext; +}): Promise { + // Replace this with the server-side lookup described above. Throwing here + // ensures that an unimplemented lookup cannot disable MFA. + throw new Error("MFA migration cohort lookup is not implemented"); +} SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // other recipes.. - Session.init({ - override: { - functions: (originalImplementation) => { + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getGlobalClaimValidators: (input) => { + // We remove the existing default MFA validator which checks that + // the client has finished MFA. + let newValidatorsArray = input.claimValidatorsAddedByOtherRecipes.filter( + (v) => v.id !== MultiFactorAuth.MultiFactorAuthClaim.key, + ); + + // We create an instance of the default validator that is added + // by SuperTokens so that we can make modifications to it + let originalValidator = + MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth(); + + // We create a custom validator based on the default validator + let customValidator = { + ...originalValidator, + validate: async (payload: any, userContext: UserContext): Promise => { + if ( + await isMfaMigrationExemptionEligible({ + tenantId: input.tenantId, + userId: input.userId, + recipeUserId: input.recipeUserId, + userContext, + }) + ) { return { - ...originalImplementation, - // highlight-start - getGlobalClaimValidators: (input) => { - // We remove the existing default MFA validator which checks that - // the client has finished MFA. - let newValidatorsArray = input.claimValidatorsAddedByOtherRecipes.filter(v => v.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - - // We create an instance of the default validator that is added - // by SuperTokens so that we can make modificationts to it - let originalValidator = MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedMFARequirementsForAuth(); - - // We create a custom validator based on the default validator - let customValidator = { - ...originalValidator, - validate: async (payload: any, userContext: UserContext): Promise => { - // We only want to run the validation check - // if we know that the client is not an older one. - // In this example, we do this based on the header - // in the API request, but the logic here can be - // anything that you like. - let request = getRequestFromUserContext(userContext); - if (request !== undefined) { - let isOlderClient = request.getHeaderValue("clilent-version") !== "2.0"; - if (isOlderClient) { - // we return true early for older clients. - return { - isValid: true - } - } - } - - // for newer clients, we call the original validate function - // which will check the claim value in the session. - return originalValidator.validate(payload, userContext); - } - } - - return [customValidator, ...newValidatorsArray]; - } - // highlight-end - } - } - } - }) - ] -}) + isValid: true, + }; + } + + // for newer clients, we call the original validate function + // which will check the claim value in the session. + return originalValidator.validate(payload, userContext); + }, + }; + + return [customValidator, ...newValidatorsArray]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import session from supertokens_python.recipe.session.utils import InputOverrideConfig @@ -118,7 +123,17 @@ from supertokens_python.types import RecipeUserId from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( MultiFactorAuthClaim, ) -from supertokens_python import get_request_from_user_context + + +async def is_mfa_migration_exemption_eligible( + tenant_id: str, + user_id: str, + recipe_user_id: RecipeUserId, + user_context: Dict[str, Any], +) -> bool: + # Replace this with the server-side lookup described above. Raising here + # ensures that an unimplemented lookup cannot disable MFA. + raise NotImplementedError("MFA migration cohort lookup is not implemented") def override_functions( @@ -149,13 +164,13 @@ def override_functions( async def custom_validate( payload: Any, user_context: Dict[str, Any] ) -> ClaimValidationResult: - # Check if the client is an older one based on the header - request = get_request_from_user_context(user_context) - if request is not None: - is_older_client = request.get_header("client-version") != "2.0" - if is_older_client: - # Return true early for older clients - return ClaimValidationResult(is_valid=True) + if await is_mfa_migration_exemption_eligible( + tenant_id, + user_id, + recipe_user_id, + user_context, + ): + return ClaimValidationResult(is_valid=True) # For newer clients, call the original validate function return await original_validator_validate_func(payload, user_context) @@ -178,18 +193,13 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ session.init(override=InputOverrideConfig(functions=override_functions)) ], ) ``` + + - - - -The code above will still result in the MFA claim being added to all sessions, with the `v` boolean in it being `false`, however, for older clients, we will not run the validator that checks if the `v` value is true on the backend since those clients have no way to show the MFA UI without updating the app. - -:::caution -You want to add the above exception only for a certain amount of time until you force all users to update their mobile app. This is because the exception method can be used as a way for malicious users to bypass MFA by spoofing that they are using an older client. -::: +The code above still adds the MFA claim to all sessions with its `v` boolean set to `false`. It skips backend validation only when the application confirms that the authenticated user and session or registered device belongs to the temporary migration cohort. Apply the original validator to every sensitive operation, regardless of cohort membership. diff --git a/docs/additional-verification/mfa/protect-routes.mdx b/docs/additional-verification/mfa/protect-routes.mdx index d1775acf75..fab9da717f 100644 --- a/docs/additional-verification/mfa/protect-routes.mdx +++ b/docs/additional-verification/mfa/protect-routes.mdx @@ -1,28 +1,25 @@ --- title: Protect frontend and backend routes -hide_title: true -sidebar_position: 8 -description: >- - Protect frontend and backend routes using SuperTokens Session Tokens and - Multi-Factor Authentication (MFA). -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Protect frontend and backend routes using SuperTokens Session Tokens and Multi-Factor Authentication (MFA). +sidebar: + order: 100 --- - -import { TokensCallout, ContextCondition } from "/src/components"; - -# Protect frontend and backend routes - ## Overview This page shows you how to protect your frontend and backend routes to make them accessible only when the user has finished all the MFA challenges configured for them. In both the backend and the frontend, routes are protected based on value of the MFA claim, in the session's access token payload. +:::caution[The backend is the authorization boundary] +Frontend MFA checks only control rendering and navigation. They can be bypassed. Every protected API must verify the +session and enforce the MFA claim on the backend; never rely on a mobile or web payload check to protect backend data. +::: + ## Before you start - +:::info +This guide only applies to scenarios involving **SuperTokens Session Access Tokens**. +::: One thing to note here is that, with **OAuth2 Access Tokens**, you don't need to check the MFA claims. You will get the token once the MFA flow is done. @@ -34,8 +31,7 @@ When you call `MultiFactorAuth.init` in the `supertokens.init` on the backend, S This validator checks that the value of `v` in the [MFA claim](./important-concepts#factors) is `true` before allowing the request to proceed. If the value of `v` is `false`, the validator will send a 403 error to the frontend. -:::important -This validator is added globally, which means that every time you use `Verify Session` or `Get Session` from the backend SDKs, this check will happen. +:::note[This validator is added globally, which means that every time you use `Verify Session` or `Get Session` from the backend SDKs, this check will happen.] This means that you don't need to add any extra code on a per API level to enforce MFA. ::: @@ -44,11 +40,24 @@ This means that you don't need to add any extra code on a per API level to enfor To exclude the default validator check in a certain backend route, you have to update `Verify Session` call. - - - - + + + + + + + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; @@ -58,52 +67,50 @@ import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let app = express(); app.post( - "/update-blog", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - }), - async (req: SessionRequest, res) => { - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + async (req: SessionRequest, res) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - }), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators - } -}) + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -112,62 +119,64 @@ import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, }), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function updateBlog(awsEvent: SessionEvent) { - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators -}; + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators +} exports.handler = verifySession(updateBlog, { - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import {SessionContext} from "supertokens-node/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - }), async (ctx: SessionContext, next) => { +router.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + async (ctx: SessionContext, next) => { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -176,106 +185,68 @@ import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; class Example { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })) - @response(200) - async handler() { - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + ) + @response(200) + async handler() { + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -// highlight-start export default async function example(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })(req, res, next); - }, - req, - res - ) - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // The user may or may not have completed the MFA required factors since we exclude - // that from the globalValidators - return NextResponse.json({}) - }, - { - // highlight-start + await superTokensNextWrapper( + async (next) => { + await verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, - // highlight-end - }); + })(req, res, next); + }, + req, + res, + ); + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })) + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + ) async postExample(@Session() session: SessionContainer): Promise { // The user may or may not have completed the MFA required factors since we exclude // that from the globalValidators @@ -283,87 +254,109 @@ export class ExampleController { } } ``` - - - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - -```python + + + + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim from supertokens_python.recipe.session import SessionContainer from fastapi import Depends -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] - # highlight-end ) )): # All validator checks have passed and the user has a verified email address pass ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] - # highlight-end ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` - - - - + + ```python from supertokens_python.recipe.session.framework.django.asyncio import verify_session from django.http import HttpRequest from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import MultiFactorAuthClaim @verify_session( - # highlight-start # We keep all validators except for the EmailVerification ones override_global_claim_validators=lambda global_validators, session, user_context: [ validators for validators in global_validators if validators.id != MultiFactorAuthClaim.key] - # highlight-end ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // The user may or may not have completed the MFA required factors since we exclude + // that from the globalValidators + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }, + ); +} +``` - - + + + + + The same modification can be done for `getSession` as well. @@ -371,503 +364,460 @@ The same modification can be done for `getSession` as well. To account for a more complex logic when you check the MFA claim (other than checking if `v` is `true`), look over the next code snippet. - - - - + + + + + + + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); app.post( - "/update-blog", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - }), - async (req: SessionRequest, res) => { - let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + async (req: SessionRequest, res) => { + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - } + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); } + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - }), + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", }, + }, ], - }, - handler: async (req: SessionRequest, res) => { - let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } - - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - } + }); } -}) + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, }), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { - // the user has finished totp + // the user has finished totp } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); } -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { - let mfaClaimValue = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } + let mfaClaimValue = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await awsEvent.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - } -}; + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); + } +} exports.handler = verifySession(updateBlog, { - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ +router.post( + "/update-blog", + verifySession({ overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); }, -}), async (ctx: SessionContext, next) => { + }), + async (ctx: SessionContext, next) => { let mfaClaimValue = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await ctx.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { - // the user has finished totp + // the user has finished totp } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); } -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; class Example { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })) - @response(200) - async handler() { - let mfaClaimValue = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } - - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - } - } -} -``` - - - - - - - - - - -```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' -import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" - -// highlight-start -export default async function example(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })(req, res, next); - }, - req, - res - ) - let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + ) + @response(200) + async handler() { + let mfaClaimValue = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await (this.ctx as any).session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; } let completedFactors = mfaClaimValue.c; if ("totp" in completedFactors) { - // the user has finished totp + // the user has finished totp } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - await superTokensNextWrapper( - async (next) => { - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", }, - req, - res - ) + }, + ], + }); } + } } ``` - - - + + + ```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; +import { superTokensNextWrapper } from "supertokens-node/nextjs"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; -import { Error as STError } from "supertokens-node/recipe/session" - -SuperTokens.init(backendConfig()); +import { Error as STError } from "supertokens-node/recipe/session"; -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } - - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - const error = new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - return NextResponse.json(error, { status: 403 }); - } - return NextResponse.json({}) +export default async function example(req: SessionRequest, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + })(req, res, next); }, - { - // highlight-start - overrideGlobalClaimValidators: async (globalValidators) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + req, + res, + ); + let mfaClaimValue = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await req.session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + await superTokensNextWrapper( + async (next) => { + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, }, - // highlight-end + ], }); + }, + req, + res, + ); + } } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); - }, - })) - async postExample(@Session() session: SessionContainer): Promise { - let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (mfaClaimValue === undefined) { - // this means that there is no MFA claim information in the session. This can happen if the session was created - // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session - // in the following way: - await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); - mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; - } + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }), + ) + async postExample(@Session() session: SessionContainer): Promise { + let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } - let completedFactors = mfaClaimValue.c; - if ("totp" in completedFactors) { - // the user has finished totp - } else { - // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a - // claim validation error in the following way: - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: "totp", - }, - }] - }) - } - return true; + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); } + return true; + } } ``` - - - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - -```python + + + + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( @@ -881,9 +831,8 @@ from supertokens_python.recipe.session.exceptions import ( from supertokens_python.recipe.session.framework.fastapi import verify_session -@app.post("/update-blog") # type: ignore +@app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): - # highlight-start mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. @@ -908,13 +857,10 @@ async def update_blog_api(session: SessionContainer = Depends(verify_session())) ], ) # If we reach here, it means the user has completed TOTP - # highlight-end ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from flask import Flask, g from supertokens_python.recipe.multifactorauth.multi_factor_auth_claim import ( @@ -929,11 +875,10 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -@app.route('/update-blog', methods=['POST']) # type: ignore +@app.route('/update-blog', methods=['POST']) @verify_session() def check_mfa_api(): - session: SessionContainer = g.supertokens # type: ignore - # highlight-start + session: SessionContainer = g.supertokens mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. @@ -952,13 +897,10 @@ def check_mfa_api(): }) ]) # If we reach here, it means the user has completed TOTP - # highlight-end ``` - - - - -```python + + +```python check=false reason="session attribute is injected by framework middleware" from typing import cast from django.http import HttpRequest @@ -976,8 +918,7 @@ from supertokens_python.recipe.session.framework.django.asyncio import verify_se @verify_session() async def get_user_info_api(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore - # highlight-start + session: SessionContainer = cast(SessionContainer, request.supertokens) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) if mfa_claim_value is None: # This means that there is no MFA claim information in the session. @@ -1002,22 +943,89 @@ async def get_user_info_api(request: HttpRequest): ], ) # If we reach here, it means the user has completed TOTP - # highlight-end ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { backendConfig } from "@/app/config/backend"; +import { Error as STError } from "supertokens-node/recipe/session"; + +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let mfaClaimValue = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (mfaClaimValue === undefined) { + // this means that there is no MFA claim information in the session. This can happen if the session was created + // prior to you enabling the MFA recipe on the backend. So here, we can add the value of the MFA claim to the session + // in the following way: + await session!.fetchAndSetClaim(MultiFactorAuth.MultiFactorAuthClaim); + mfaClaimValue = (await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim))!; + } + + let completedFactors = mfaClaimValue.c; + if ("totp" in completedFactors) { + // the user has finished totp + } else { + // the user has not finished totp. You can choose to do anything you like here, for example, we may throw a + // claim validation error in the following way: + const error = new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: "totp", + }, + }, + ], + }); + return NextResponse.json(error, { status: 403 }); + } + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async (globalValidators) => { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.key); + }, + }, + ); +} +``` - - + + + + + - In the code snippet above, we remove the default validator that was added to the global validators (which checks if the `v` value in the claim is true or not). You don't need to do this, but in the code snippet above, we show it anyway. - Then in the API logic, we manually fetch the claim value, and then check if TOTP has been completed or not. If it hasn't, we send back a 403 error to the frontend. You can use a similar approach as shown above to do any kind of check. -:::info important -If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true`. +:::info[important] +If you are doing JWT verification manually, then post verification, you should check the payload of the JWT and make sure that the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true`. This would be equivalent to doing a check as our default claim validator mentioned above. Make sure to also do other checks on the JWT payload. For example, if you require all users to have finished email verification, then we need to check for that claim as well in the JWT. @@ -1027,223 +1035,213 @@ Make sure to also do other checks on the JWT payload. For example, if you requir ## Protect frontend routes - - - + - + - - -When you call `MultiFactorAuth.init` in the `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you use the `SessionAuth` component. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. + + +When you call `MultiFactorAuth.init` in the `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you use the `SessionAuth` component. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. If it is not, then the user will be redirected to the MFA auth screen. ### Other forms of authorization If you do not want to run our default validator on a specific route, you can modify the use of `SessionAuth` in the following way: - + + +By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. + + + + + ```tsx import React from "react"; -import { SessionAuth, useSessionContext, useClaimValue } from 'supertokens-auth-react/recipe/session'; +import { SessionAuth, useSessionContext, useClaimValue } from "supertokens-auth-react/recipe/session"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; const VerifiedRoute = (props: React.PropsWithChildren) => { - return ( - { - return globalValidators.filter(validator => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id); - }}> - - {props.children} - - - ); -} + return ( + { + return globalValidators.filter((validator) => validator.id !== MultiFactorAuth.MultiFactorAuthClaim.id); + }} + > + {props.children} + + ); +}; function InvalidClaimHandler(props: React.PropsWithChildren) { + const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - - if (claimValue.loading) { - return null; - } + if (claimValue.loading) { + return null; + } - if (claimValue.value === undefined || !("totp" in claimValue.value.c)) { - return
You do not have access to this page because you have not completed TOTP. Please click here to finish to proceed.
- } + if (claimValue.value === undefined || !("totp" in claimValue.value.c)) { + return ( +
+ You do not have access to this page because you have not completed TOTP. Please{" "} + click here to finish to proceed. +
+ ); + } - // the user has finished TOTP, so we can render the children - return
{props.children}
; + // the user has finished TOTP, so we can render the children + return
{props.children}
; } ``` -- In the snippet above, we remove the default claim validator that is added to `SessionAuth`, and add out own logic that reads from the session's payload. -- Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks. - -
- - - -By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. - + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - let validationErrors = await Session.validateClaims(); + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); - if (validationErrors.length === 0) { - // user has finished all MFA factors. - return true; - } else { - for (const err of validationErrors) { - if (err.id === MultiFactorAuthClaim.id) { - // user has not finished MFA factors. - let mfaClaimValue = await Session.getClaimValue({ - claim: MultiFactorAuthClaim - }); - if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { - // the user has not finished totp - return false; - } - } - } + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.id === MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await Session.getClaimValue({ + claim: MultiFactorAuthClaim, + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } } + } } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` + + + + +- In the snippet above, we remove the default claim validator that is added to `SessionAuth`, and add out own logic that reads from the session's payload. +- Finally, we check if the user has completed TOTP or not. If not, we show a message to the user, and ask them to complete TOTP. Of course, if this is all you want to do, then the default validator already does that. But the above has the boilerplate for how you can do more complex checks. + + In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. + + - - +
-
+ -
- - - - -By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) is `true` or not. - - - + + +By default, when you do `MultiFactorAuth.init` in `supertokens.init` on the frontend, SuperTokens will add a default validator check that runs whenever you call the `Session.validateClaims` function. This validator checks if the `v` value in the [MFA claim](./important-concepts#factor-completion-status) is `true` or not. + + +The examples below read the raw access token payload for user-interface decisions only. They do not use or imply a +mobile-specific MFA authorization API. Backend MFA claim validation remains required for protected APIs. + + + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - let validationErrors = await Session.validateClaims(); + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); - if (validationErrors.length === 0) { - // user has finished all MFA factors. - return true; - } else { - for (const err of validationErrors) { - if (err.id === MultiFactorAuthClaim.id) { - // user has not finished MFA factors. - let mfaClaimValue = await Session.getClaimValue({ - claim: MultiFactorAuthClaim - }); - if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { - // the user has not finished totp - return false; - } - } - } + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.id === MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await Session.getClaimValue({ + claim: MultiFactorAuthClaim, + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } } + } } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - let validationErrors = await supertokensSession.validateClaims(); + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims(); - if (validationErrors.length === 0) { - // user has finished all MFA factors. - return true; - } else { - for (const err of validationErrors) { - if (err.id === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) { - // user has not finished MFA factors. - let mfaClaimValue = await supertokensSession.getClaimValue({ - claim: supertokensMultiFactorAuth.MultiFactorAuthClaim - }); - if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { - // the user has not finished totp - return false; - } - } - } + if (validationErrors.length === 0) { + // user has finished all MFA factors. + return true; + } else { + for (const err of validationErrors) { + if (err.id === supertokensMultiFactorAuth.MultiFactorAuthClaim.id) { + // user has not finished MFA factors. + let mfaClaimValue = await supertokensSession.getClaimValue({ + claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, + }); + if (mfaClaimValue === undefined || !("totp" in mfaClaimValue.c)) { + // the user has not finished totp + return false; + } } + } } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` - - - - -In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. - -In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. - - - - - -In your app, you can check the [MFA claim](./important-concepts#how-are-auth-factors-marked-as-completed) values to know which factors have been completed by the user and take action based on that. - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function checkIfMFAIsCompleted() { - if (await SuperTokens.doesSessionExist()) { - - // highlight-start - let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + if (await SuperTokens.doesSessionExist()) { + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; - if (isMFACompleted) { - // All required factors for MFA have been completed - } else { - // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user - } - // highlight-end + if (isMFACompleted) { + // All required factors for MFA have been completed + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -1261,11 +1259,8 @@ class MainApplication: Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -1289,11 +1284,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -1315,27 +1307,33 @@ Future checkIfMFAIsCompleted() async { } } ``` + + + + - - - + + +In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. This function inspects the session's contents and runs claim validators on them. If a claim validator fails, it will be reflected in the `validationErrors` variable. The `MultiFactorAuthClaim` validator will be automatically checked by this function since you have initialized the MFA recipe. -If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](./frontend-setup#mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again. +In case the claim fails, you can get the claim value and check which factor is not completed. In the above code, we check that if it's the TOTP factor that is missing when the claim fails and return `false` from this function. However, it's really up to you for what you want to do next. For example, you could redirect the user to the TOTP factor screen. + + +If the MFA claim value is missing in the access token payload, then it means that the session was created before you enabled MFA on the backend. In this case, you can call the [MFA Info](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) endpoint which will add the MFA claim to the session and check again. + + - - - + --- ## See also - - - - - - - + + + + + + diff --git a/docs/additional-verification/mfa/step-up-auth.mdx b/docs/additional-verification/mfa/step-up-auth.mdx index 917162b568..9debe49a27 100644 --- a/docs/additional-verification/mfa/step-up-auth.mdx +++ b/docs/additional-verification/mfa/step-up-auth.mdx @@ -1,17 +1,10 @@ --- title: Implement step-up authentication -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 7 description: Implement step-up authentication using SuperTokens MFA recipe. -page_type: guide -recipe: mfa -category: multi-factor-authentication +sidebar: + order: 80 --- - -# Implement step-up authentication - ## Overview Step-up authentication enforces the user to complete an authentication challenge before navigating to a page, or before doing a specific action. @@ -22,7 +15,7 @@ You can implement it with **SuperTokens** as full page navigation, or as popups These instructions assume that you already have some knowledge of MFA. -If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/docs/additional-verification/mfa/important-concepts). +If you are not familiar with terms like authentication factors and challenges, please go through the [MFA concepts page](/additional-verification/mfa/important-concepts). ### Prerequisites @@ -43,384 +36,344 @@ To protect sensitive APIs with step up auth, you need to check that the user has If they haven't, you should return a `403` to the frontend which highlights which factor is necessary. The frontend can then consume this and show the auth challenge to the user. - - - - + + + + + + + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); -app.post( - "/update-blog", - verifySession(), - async (req: SessionRequest, res) => { - let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... - } -); +app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => { + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); + } + // continue with API logic... +}); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession(), + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, }, + }, ], - }, - handler: async (req: SessionRequest, res) => { - let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... - + }); } -}) + // continue with API logic... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession(), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); } // continue with API logic... -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { - let mfaClaim = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... -}; + let mfaClaim = await awsEvent.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); + } + // continue with API logic... +} exports.handler = verifySession(updateBlog); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { - let mfaClaim = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... + let mfaClaim = await ctx.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); + } + // continue with API logic... }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; class Example { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession()) - @response(200) - async handler() { - let mfaClaim = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept(verifySession()) + @response(200) + async handler() { + let mfaClaim = await (this.ctx as any).session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); } + // continue with API logic... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; -// highlight-start export default async function example(req: SessionRequest, res: any) { + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); + let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) - let mfaClaim = await req.session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - await superTokensNextWrapper( - async (next) => { - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) + async (next) => { + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, }, - req, - res - ) - } - // continue with API logic... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; -import { Error as STError } from "supertokens-node/recipe/session" - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - const error = new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - return NextResponse.json(error, { status: 403 }); - } - // continue with API logic... - return NextResponse.json({}) - }); + ], + }); + }, + req, + res, + ); + } + // continue with API logic... } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard()) - async postExample(@Session() session: SessionContainer): Promise { - let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000*60*5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - // continue with API logic... - return true; + @Post("example") + @UseGuards(new AuthGuard()) + async postExample(@Session() session: SessionContainer): Promise { + let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); } + // continue with API logic... + return true; + } } ``` - - - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - -```python + + + + + + + + + +```python check=false reason="route fragment assumes an existing framework application" import time from fastapi import Depends @@ -436,14 +389,13 @@ from supertokens_python.recipe.session.exceptions import ( from supertokens_python.recipe.session.framework.fastapi import verify_session -@app.post("/update-blog") # type: ignore +@app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): - # highlight-start mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") - if totp_completed_time is None or totp_completed_time < (round(time.time() * 1000) - 1000*60*5): + if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", @@ -458,13 +410,10 @@ async def update_blog_api(session: SessionContainer = Depends(verify_session())) ], ) # If we reach here, it means the user has completed TOTP - # highlight-end ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" import time from flask import Flask, g @@ -481,16 +430,15 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -@app.route('/update-blog', methods=['POST']) # type: ignore +@app.route('/update-blog', methods=['POST']) @verify_session() def check_mfa_api(): - session: SessionContainer = g.supertokens # type: ignore - # highlight-start + session: SessionContainer = g.supertokens mfa_claim_value = session.sync_get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") - if totp_completed_time is None or totp_completed_time < (round(time.time() * 1000) - 1000*60*5): + if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", @@ -505,13 +453,10 @@ def check_mfa_api(): ], ) # If we reach here, it means the user has completed TOTP - # highlight-end ``` - - - - -```python + + +```python check=false reason="session attribute is injected by framework middleware" import time from typing import cast @@ -530,13 +475,12 @@ from supertokens_python.recipe.session.framework.django.asyncio import verify_se @verify_session() async def get_user_info_api(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore - # highlight-start + session: SessionContainer = cast(SessionContainer, request.supertokens) mfa_claim_value = await session.get_claim_value(MultiFactorAuthClaim) assert mfa_claim_value is not None totp_completed_time = mfa_claim_value.c.get("totp") - if totp_completed_time is None or totp_completed_time < (round(time.time() * 1000) - 1000*60*5): + if totp_completed_time is None or totp_completed_time < (int(time.time()) - 5 * 60): # TOTP hasn't been completed or was completed more than 5 minutes ago raise_invalid_claims_exception( "TOTP validation required", @@ -551,14 +495,64 @@ async def get_user_info_api(request: HttpRequest): ], ) # If we reach here, it means the user has completed TOTP - # highlight-end ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { backendConfig } from "@/app/config/backend"; +import { Error as STError } from "supertokens-node/recipe/session"; - - +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let mfaClaim = await session!.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + const totpCompletedTime = mfaClaim!.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + const error = new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); + return NextResponse.json(error, { status: 403 }); + } + // continue with API logic... + return NextResponse.json({}); + }); +} +``` + + + + + + - When calling the `verifySession`, SuperTokens makes sure that the session is valid and that the user has completed all the required auth factors at some point in time. This enforces the basic check that the user has finished MFA during login. - Further check if the user has finished the TOTP login method within the last 5 minutes. If they haven't, send back a 403 to the frontend for the frontend to handle. @@ -576,82 +570,86 @@ This opens up a security issue when it comes to completing step up auth. Conside This allows someone malicious to bypass step up auth. To prevent this, override one of the MFA recipe functions on the backend. This enforces that the factor setup can only happen if the user is not in a step-up auth state. - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import { Error as STError } from "supertokens-node/recipe/session" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import { Error as STError } from "supertokens-node/recipe/session"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - - MultiFactorAuth.init({ - firstFactors: [/*...*/], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // highlight-start - assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { - await originalImplementation.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); - - let claimValue = await input.session.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (claimValue === undefined || !claimValue.v) { - return - } - - // if the above did not throw, it means that the user has logged in and has completed all the required - // factors for login. So now we check specifically for the step up auth case: - if (input.factorId === MultiFactorAuth.FactorIds.TOTP && (await input.factorsSetUpForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { - // this is an example of checking for totp, but you can also use other factor IDs. - const totpCompletedTime = claimValue.c[MultiFactorAuth.FactorIds.TOTP]; - if (totpCompletedTime === undefined || totpCompletedTime < (Date.now() - 1000 * 60 * 5)) { - // this means that the user had completed the TOTP challenge more than 5 minutes ago - // so we should ask them to complete it again - throw new STError({ - type: "INVALID_CLAIMS", - message: "User has not finished TOTP", - payload: [{ - id: MultiFactorAuth.MultiFactorAuthClaim.key, - reason: { - message: "Factor validation failed: totp not completed", - factorId: MultiFactorAuth.FactorIds.TOTP, - }, - }] - }) - } - } - } - // highlight-end - } + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + MultiFactorAuth.init({ + firstFactors: [ + /*...*/ + ], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + assertAllowedToSetupFactorElseThrowInvalidClaimError: async (input) => { + await originalImplementation.assertAllowedToSetupFactorElseThrowInvalidClaimError(input); + + let claimValue = await input.session.getClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (claimValue === undefined || !claimValue.v) { + return; + } + + // if the above did not throw, it means that the user has logged in and has completed all the required + // factors for login. So now we check specifically for the step up auth case: + if ( + input.factorId === MultiFactorAuth.FactorIds.TOTP && + (await input.factorsSetUpForUser).includes(MultiFactorAuth.FactorIds.TOTP) + ) { + // this is an example of checking for totp, but you can also use other factor IDs. + const totpCompletedTime = claimValue.c[MultiFactorAuth.FactorIds.TOTP]; + if (totpCompletedTime === undefined || totpCompletedTime < Math.floor(Date.now() / 1000) - 5 * 60) { + // this means that the user had completed the TOTP challenge more than 5 minutes ago + // so we should ask them to complete it again + throw new STError({ + type: "INVALID_CLAIMS", + message: "User has not finished TOTP", + payload: [ + { + id: MultiFactorAuth.MultiFactorAuthClaim.key, + reason: { + message: "Factor validation failed: totp not completed", + factorId: MultiFactorAuth.FactorIds.TOTP, + }, + }, + ], + }); } - } - }) - ] -}) + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" import time from typing import Any, Awaitable, Callable, Dict, List @@ -704,7 +702,7 @@ def override_functions(original_implementation: RecipeInterface): ): totp_completed_time = claim_value.c.get(FactorIds.TOTP) if totp_completed_time is None or totp_completed_time < ( - int(time.time() * 1000) - 1000 * 60 * 5 + int(time.time()) - 5 * 60 ): # User completed TOTP challenge more than 5 minutes ago raise_invalid_claims_exception( @@ -735,7 +733,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], @@ -744,9 +742,8 @@ init( ], ) ``` - - - + + - SuperTokens calls the function `assertAllowedToSetupFactorElseThrowInvalidClaimError` whenever the client calls an API to setup a new factor (for example, create a new TOTP device). Perform checks in this function and throw an error if necessary to prevent factor setup. - In the override logic, first call the original implementation and check that the `v` value in the MFA session claim is `true`. This throws / exits the function early if the user has not logged in yet (for example, they have finished the first factor, but not the required second factor). @@ -766,16 +763,16 @@ The JSON body of the step-up auth claim failure looks like this: "id": "st-mfa", "reason": { "message": "Factor validation failed: totp not completed", - "factorId": "totp", + "factorId": "totp" } } ] } ``` - + - + You can check for this structure and the `factorId` to decide what factor to show on the frontend. You have two options to show the UI to the user: @@ -783,46 +780,51 @@ You can check for this structure and the `factorId` to decide what factor to sho To redirect the user to as factor challenge page and then navigate them back to the current page, you can use the following function: - - + + +Redirect the user to `/{websiteBasePath}/mfa/totp?stepUp=true&redirectToPath={currentPath}`. This shows the TOTP +challenge in step-up mode. The `redirectToPath` query parameter tells the SDK to redirect the user back to the current +page after they complete the challenge. + + + + ```tsx -import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth'; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { - MultiFactorAuth.redirectToFactor({ - factorId: "totp", - stepUp: true, - redirectBack: true, - }) + MultiFactorAuth.redirectToFactor({ + factorId: "totp", + stepUp: true, + redirectBack: true, + }); } ``` + + -- In the snippet above, redirect to the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup). Set the `stepUp` argument to `true` otherwise the MFA screen would detect that the user has already completed basic MFA requirements and would not show the verification screen. Set the `redirectBack` argument to `true` since the intention is to redirect back to the current page after the user has finished setting up the device. -- You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. + + - - - - - -To add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This shows the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup) to the user. The `redirectToPath` query parameter also tells the SDK to redirect the user back to the current page after they have finished creating the device. - - - - + + +- In the snippet above, redirect to the [TOTP factor setup screen](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/totp-mfa--device-setup). Set the `stepUp` argument to `true` otherwise the MFA screen would detect that the user has already completed basic MFA requirements and would not show the verification screen. Set the `redirectBack` argument to `true` since the intention is to redirect back to the current page after the user has finished setting up the device. +- You can also redirect the user to `/{websiteBasePath}/mfa/totp?stepUp=true&redirectToPath={currentPath}` if you don't want to use the above function. + + #### Show the factor in a popup -Checkout [the documentation](/docs/additional-verification/mfa/embed-the-prebuilt-ui) for embedding the pre-built UI factor components in a page or a popup. +Checkout [the documentation](/additional-verification/mfa/embed-the-prebuilt-ui) for embedding the pre-built UI factor components in a page or a popup. - + - + You can check for this structure and the `factorId` to decide what factor to show on the frontend. - + ### 4. Check for step-up authentication on page navigation @@ -834,195 +836,195 @@ To do this, read the access token payload on the frontend and check the complete If the completed time is older than 5 minutes (as an example), redirect the user to the factor challenge page. - - - - - + + + ```tsx import React from "react"; -import { SessionAuth, useClaimValue } from 'supertokens-auth-react/recipe/session'; +import { SessionAuth, useClaimValue } from "supertokens-auth-react/recipe/session"; import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; -import { DateProviderReference } from "supertokens-auth-react/utils/dateProvider" +import { DateProviderReference } from "supertokens-auth-react/utils/dateProvider"; const VerifiedRoute = (props: React.PropsWithChildren) => { - return ( - - - {props.children} - - - ); -} + return ( + + {props.children} + + ); +}; function InvalidClaimHandler(props: React.PropsWithChildren) { - let claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); - if (claimValue.loading) { - return null; - } + let claimValue = useClaimValue(MultiFactorAuth.MultiFactorAuthClaim); + if (claimValue.loading) { + return null; + } - let totpCompletedTime = claimValue.value?.c[MultiFactorAuth.FactorIds.TOTP] - if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { - return
You need to complete TOTP before seeing this page. Please click here to finish to proceed.
- } + let totpCompletedTime = claimValue.value?.c[MultiFactorAuth.FactorIds.TOTP]; + if ( + totpCompletedTime === undefined || + totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 + ) { + return ( +
+ You need to complete TOTP before seeing this page. Please{" "} + click here to finish to + proceed. +
+ ); + } - // the user has finished TOTP, so we can render the children - return
{props.children}
; + // the user has finished TOTP, so we can render the children + return
{props.children}
; } ``` -- Check if the user has completed TOTP within the last 5 minutes or not. If not, show a message to the user, and ask them to complete TOTP. -- Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. - -
- - - + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - let validationErrors = await Session.validateClaims(); - - if (validationErrors.length === 0) { - // since all default claim validators have passed, we now check for if the user has finished TOTP - // within the last 5 mins - let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); - let totpCompletedTime = mfaClaimValue?.c["totp"]; - if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { - // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. - return false; - } - return true; - } else { - // handle other validation failure events... - } + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if ( + totpCompletedTime === undefined || + totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 + ) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` + + + + +- Check if the user has completed TOTP within the last 5 minutes or not. If not, show a message to the user, and ask them to complete TOTP. +- Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. + + - In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. - Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. + + - + -
+ -
- - - - - - - + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { MultiFactorAuthClaim } from "supertokens-web-js/recipe/multifactorauth"; import { DateProviderReference } from "supertokens-web-js/utils/dateProvider"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - let validationErrors = await Session.validateClaims(); - - if (validationErrors.length === 0) { - // since all default claim validators have passed, we now check for if the user has finished TOTP - // within the last 5 mins - let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); - let totpCompletedTime = mfaClaimValue?.c["totp"]; - if (totpCompletedTime === undefined || totpCompletedTime < (DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { - // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. - return false; - } - return true; - } else { - // handle other validation failure events... - } + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await Session.getClaimValue({ claim: MultiFactorAuthClaim }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if ( + totpCompletedTime === undefined || + totpCompletedTime < Math.floor(DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000) - 5 * 60 + ) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensMultiFactorAuth from "supertokens-web-js-script/recipe/multifactorauth"; -import supertokensDateProviderReference from "supertokens-web-js-script/utils/dateProvider"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - let validationErrors = await supertokensSession.validateClaims(); - - if (validationErrors.length === 0) { - // since all default claim validators have passed, we now check for if the user has finished TOTP - // within the last 5 mins - let mfaClaimValue = await supertokensSession.getClaimValue({ claim: supertokensMultiFactorAuth.MultiFactorAuthClaim }); - let totpCompletedTime = mfaClaimValue?.c["totp"]; - if (totpCompletedTime === undefined || totpCompletedTime < (supertokensDateProviderReference.DateProviderReference.getReferenceOrThrow().dateProvider.now() - 1000 * 60 * 5)) { - // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. - return false; - } - return true; - } else { - // handle other validation failure events... - } + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims(); + + if (validationErrors.length === 0) { + // since all default claim validators have passed, we now check for if the user has finished TOTP + // within the last 5 mins + let mfaClaimValue = await supertokensSession.getClaimValue({ + claim: supertokensMultiFactorAuth.MultiFactorAuthClaim, + }); + let totpCompletedTime = mfaClaimValue?.c["totp"]; + if ( + totpCompletedTime === undefined || + totpCompletedTime < + Math.floor( + supertokensDateProviderReference.DateProviderReference.getReferenceOrThrow().dateProvider.now() / 1000, + ) - + 5 * 60 + ) { + // ths user needs to complete TOTP since it's been more than 5 mins since they completed it. + return false; + } + return true; + } else { + // handle other validation failure events... } - // a session does not exist, or email is not verified - return false + } + // a session does not exist, or email is not verified + return false; } ``` - - - - -- In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. -- For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. -- Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function checkIfMFAIsCompleted() { - if (await SuperTokens.doesSessionExist()) { - - // highlight-start - let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; - if (isMFACompleted) { - let completedFactors = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].c; - if (completedFactors["totp"] === undefined || completedFactors["totp"] < (Date.now() - 1000*60*5)) { - // user has not finished TOTP MFA in the last 5 minutes - } - } else { - // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user - } - // highlight-end + if (await SuperTokens.doesSessionExist()) { + let isMFACompleted: boolean = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].v; + if (isMFACompleted) { + let completedFactors = (await SuperTokens.getAccessTokenPayloadSecurely())["st-mfa"].c; + if (completedFactors["totp"] === undefined || completedFactors["totp"] < Math.floor(Date.now() / 1000) - 5 * 60) { + // user has not finished TOTP MFA in the last 5 minutes + } + } else { + // You can check the `c` object from ["st-mfa"] prop to see which factors have been completed by the user } + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -1053,11 +1055,8 @@ class MainApplication: Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -1081,11 +1080,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -1113,28 +1109,34 @@ Future checkIfMFAIsCompleted() async { } } ``` + + + + - - - - + + +- In your protected routes, you need to first check if a session exists, and then call the Session.validateClaims function as shown above. If that passes, it means all the default claim validators have passed (checks applied to all routes in general), and then perform the step-up auth check. +- For checking for step-up auth, get the MFA claim value from the session and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. +- Notice that the `DateProviderReference` class exported by SuperTokens replaces `Date.now()`. This accounts for any clock skew that may exist between the frontend and the backend server. + + - In your protected routes, you need to first check if a session exists, and then check that the user has finished all the basic MFA factors for logging in (by checking the value of the `v` boolean in the MFA claim session). If that passes, then perform the step-up auth check. - For checking for step-up auth, get the MFA claim value from the session, and then check if TOTP completed within the last 5 minutes. Only if it did, return true, else return false. + + - - - + --- ## See also - - - - - - - + + + + + + diff --git a/docs/additional-verification/mfa/totp/_category_.json b/docs/additional-verification/mfa/totp/_category_.json deleted file mode 100644 index a4a93e6285..0000000000 --- a/docs/additional-verification/mfa/totp/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "TOTP", - "position": 5 -} diff --git a/docs/additional-verification/mfa/totp/meta.ts b/docs/additional-verification/mfa/totp/meta.ts new file mode 100644 index 0000000000..c1a6fbfcf9 --- /dev/null +++ b/docs/additional-verification/mfa/totp/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "TOTP", + icon: "hash", + order: 40, +}); diff --git a/docs/additional-verification/mfa/totp/totp-for-all-users.mdx b/docs/additional-verification/mfa/totp/totp-for-all-users.mdx index 24803dd02e..9b81e87efb 100644 --- a/docs/additional-verification/mfa/totp/totp-for-all-users.mdx +++ b/docs/additional-verification/mfa/totp/totp-for-all-users.mdx @@ -1,18 +1,10 @@ --- title: Require TOTP for all users -hide_title: true -sidebar_position: 1 -description: >- - Implement a TOTP-based MFA policy for all users to enhance application - security. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement a TOTP-based MFA policy for all users to enhance application security. +sidebar: + order: 1 --- - -# TOTP required for all users - ## Overview This guide shows you how to implement an MFA policy that requires all users to use TOTP before they get access to your application. @@ -25,77 +17,71 @@ The tutorial assumes that the first factor is email password or social login, bu ## Steps - + - + ### 1. Configure the backend To start with, we configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-next-line - totp.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - return [MultiFactorAuth.FactorIds.TOTP] - } - } - } - } - // highlight-end - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + return [MultiFactorAuth.FactorIds.TOTP]; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( @@ -137,7 +123,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ totp.init(), multifactorauth.init( @@ -147,9 +133,8 @@ init( ], ) ``` - - - + + - Notice that we have initialised the TOTP recipe in the `recipeList`. By default, no configs are required for it, but you can provide: - `issuer`: This is the name that will show up in the TOTP app for the user. By default, this is equal to the `appName` config, however, you can change it to something else using this property. @@ -160,12 +145,12 @@ init( Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -173,13 +158,13 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "totp": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } } ``` @@ -187,118 +172,113 @@ Indicating that the user has finished all required factors, and should be allowe ### 2. Configure the frontend - + - + We start by modifying the `init` function call on the frontend like so: - + + +You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - +This change is in your auth route config. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import Passwordless from "supertokens-auth-react/recipe/passwordless" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import totp from "supertokens-auth-react/recipe/totp" +import supertokens from "supertokens-auth-react"; +import Passwordless from "supertokens-auth-react/recipe/passwordless"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import totp from "supertokens-auth-react/recipe/totp"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - totp.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + totp.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` - - - - - -You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - -This change is in your auth route config. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless" -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUITOTP from "supertokens-auth-react-script/recipe/totp" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - supertokensUITOTP.init(), - supertokensUIMultiFactorAuth.init({ - firstFactors: [ - supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, - supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + supertokensUITOTP.init(), + supertokensUIMultiFactorAuth.init({ + firstFactors: [ + supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, + supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, + ], + }), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK config at the root of your application: + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Totp from "supertokens-web-js/recipe/totp"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - Totp.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + Totp.init(), + ], }); ``` - - - - + + - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe, and pass in the first factors that we want to use. In this case, that would be `emailpassword` and `thirdparty` - same as the backend. Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: - - - - - - + + +:::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] +::: + + + + + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; @@ -313,9 +293,10 @@ function App() {
- // highlight-start - {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-end + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ + /* ... */ TOTPPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} // ... other routes
@@ -325,11 +306,8 @@ function App() { ); } ``` - -
- - - + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; @@ -337,116 +315,91 @@ import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; function App() { - // highlight-start if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { - return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]) + return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]); } - // highlight-end - return ( - {/*Your app*/} - ); + return {/*Your app*/}; } ``` + + + + - - -
- -
- - - -:::success -This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. -::: - - - -
+ + With the above configuration, users will see `emailpassword` or social login UI when they visit the auth page. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before). -
+
- + -We start by initialising the MFA and TOTP recipe on the frontend like so: - +We start by initialising the MFA and TOTP recipe on the frontend like so: - - - + + +:::success[This step is not applicable for mobile apps. Please continue reading.] +::: + + + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; import Totp from "supertokens-web-js/recipe/totp"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - Totp.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + Totp.init(), + ], }); ``` - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import supertokensTotp from "supertokens-web-js-script/recipe/totp"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - supertokensMultiFactorAuth.init(), - supertokensTotp.init() - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + supertokensMultiFactorAuth.init(), + supertokensTotp.init(), + ], }); ``` + + + + - + + - - +After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](/additional-verification/mfa/initial-setup#12-add-the-mfa-flow). If it's not, then we can redirect the user to the application page. - - -:::success -This step is not applicable for mobile apps. Please continue reading. -::: - - - - - -After the first factor login, you should start by [checking the access token payload and see if the MFA claim's `v` boolean is `false`](../frontend-setup#step-2-checking-the---custv-boolean-value-in-the-mfa-claim--cust). If it's not, then we can redirect the user to the application page. - -If it's `false`, the frontend then needs to [call the MFA endpoint](../frontend-setup#mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["totp"]`. +If it's `false`, the frontend then needs to [call the MFA endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint) to get information about which factor the user should be asked to complete next. Based on the backend config in this page, the `next` array will contain `["totp"]`. Two possibilities exist here: - Case 1: The user needs to setup a TOTP device cause they don't have any. @@ -461,96 +414,90 @@ In this case, we do two things: To create a new device, call the following API: - - - - - + + +The above API call returns the following response: + + + + + + ```tsx -import Totp from "supertokens-web-js/recipe/totp" -import Session from "supertokens-web-js/recipe/session" +import Totp from "supertokens-web-js/recipe/totp"; +import Session from "supertokens-web-js/recipe/session"; async function createNewTotpDevice() { - if (await Session.doesSessionExist()) { - try { - let deviceResponse = await Totp.createDevice(); - if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { - // this should only come here if you are passing a custom device name when calling the above function. - throw new Error("Should never come here") - // device created successfully - } - // device created successfully - let qrCodeString = deviceResponse.qrCodeString; - let secret = deviceResponse.secret; - - // TODO: display a QR code based on qrCodeString, and also an option to view - // the secret if the user is unable to scan the QR code. - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") + if (await Session.doesSessionExist()) { + try { + let deviceResponse = await Totp.createDevice(); + if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { + // this should only come here if you are passing a custom device name when calling the above function. + throw new Error("Should never come here"); + // device created successfully + } + // device created successfully + let qrCodeString = deviceResponse.qrCodeString; + let secret = deviceResponse.secret; + + // TODO: display a QR code based on qrCodeString, and also an option to view + // the secret if the user is unable to scan the QR code. + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP device creation can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - -```tsx -import supertokensTotp from "supertokens-web-js-script/recipe/totp" -import supertokensSession from "supertokens-web-js-script/recipe/session" + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function createNewTotpDevice() { - if (await supertokensSession.doesSessionExist()) { - try { - let deviceResponse = await supertokensTotp.createDevice(); - if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { - // this should only come here if you are passing a custom device name when calling the above function. - throw new Error("Should never come here") - // device created successfully - } - // device created successfully - let qrCodeString = deviceResponse.qrCodeString; - let secret = deviceResponse.secret; - - // TODO: display a QR code based on qrCodeString, and also an option to view - // the secret if the user is unable to scan the QR code. - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP device creation can only happen after the first factor is complete and when a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + let deviceResponse = await supertokensTotp.createDevice(); + if (deviceResponse.status === "DEVICE_ALREADY_EXISTS_ERROR") { + // this should only come here if you are passing a custom device name when calling the above function. + throw new Error("Should never come here"); + // device created successfully + } + // device created successfully + let qrCodeString = deviceResponse.qrCodeString; + let secret = deviceResponse.secret; + + // TODO: display a QR code based on qrCodeString, and also an option to view + // the secret if the user is unable to scan the QR code. + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP device creation can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - - - - - - - -The above API call returns the following response: - -```json + + + + +```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "issuerName": "...", @@ -562,10 +509,10 @@ The above API call returns the following response: "status": "DEVICE_ALREADY_EXISTS_ERROR" | "GENERAL_ERROR" } ``` + + - - - When device registration is successful, the API returns: - The `secret` and `qrCodeString` which are to be displayed to the user. For React apps, we recommend using the [react-qr-code library](https://github.com/rosskhanas/react-qr-code) to display the QR code. @@ -576,103 +523,96 @@ The above API call returns the following response: Once a device has been created, and scanned, you need to ask the user to enter the TOTP and call the API below to verify it: - - - - - + + +The above API call returns the following response: + + + + + + ```tsx -import Totp from "supertokens-web-js/recipe/totp" -import Session from "supertokens-web-js/recipe/session" +import Totp from "supertokens-web-js/recipe/totp"; +import Session from "supertokens-web-js/recipe/session"; async function verifyTotpDevice(deviceName: string, userInputTotp: string) { - if (await Session.doesSessionExist()) { - try { - let verifyResponse = await Totp.verifyDevice({ - deviceName, - totp: userInputTotp, - }); - if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { - // this can happen due to a race condition wherein the device is deleted before verifying. - window.alert("Something went wrong. Please reload and try again"); - } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { - // this can happen if the user has entered a wrong TOTP too many times. - window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); - } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { - window.alert("Totp incorrect. Please try again"); - } else { - // Device verified successfully - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") + if (await Session.doesSessionExist()) { + try { + let verifyResponse = await Totp.verifyDevice({ + deviceName, + totp: userInputTotp, + }); + if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { + // this can happen due to a race condition wherein the device is deleted before verifying. + window.alert("Something went wrong. Please reload and try again"); + } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Device verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP device verification can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - -```tsx -import supertokensTotp from "supertokens-web-js-script/recipe/totp" -import supertokensSession from "supertokens-web-js-script/recipe/session" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function verifyTotpDevice(deviceName: string, userInputTotp: string) { - if (await supertokensSession.doesSessionExist()) { - try { - let verifyResponse = await supertokensTotp.verifyDevice({ - deviceName, - totp: userInputTotp, - }); - if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { - // this can happen due to a race condition wherein the device is deleted before verifying. - window.alert("Something went wrong. Please reload and try again"); - } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { - // this can happen if the user has entered a wrong TOTP too many times. - window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); - } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { - window.alert("Totp incorrect. Please try again"); - } else { - // Device verified successfully - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP device verification can only happen after the first factor is complete and when a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + let verifyResponse = await supertokensTotp.verifyDevice({ + deviceName, + totp: userInputTotp, + }); + if (verifyResponse.status === "UNKNOWN_DEVICE_ERROR") { + // this can happen due to a race condition wherein the device is deleted before verifying. + window.alert("Something went wrong. Please reload and try again"); + } else if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Device verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP device verification can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - - - - - - - -The above API call returns the following response: - -```json + + + + +```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "wasAlreadyVerified": false @@ -687,115 +627,108 @@ The above API call returns the following response: "status": "UNKNOWN_DEVICE_ERROR" | "GENERAL_ERROR" } ``` + + - - - The `deviceName`, which is an input to the API is one of the props returned from the previous API call to create a device. - When verification is successful (`status: "OK"`), the device is marked as verified in the database and can be used for the TOTP challenge next time around. The boolean `wasAlreadyVerified` indicates if the device was already verified before this call was made. -- A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: +- A status of `INVALID_TOTP_ERROR` means that the user has entered an incorrect TOTP and needs to retry. The response contains two other props: - `currentNumberOfFailedAttempts`: The number of times the user has entered an incorrect TOTP so far. - `maxNumberOfFailedAttempts`: The maximum number of times the user can enter an incorrect TOTP before they are asked to wait (see `status: LIMIT_REACHED_ERROR`). This is set to 5 by default in the core. You can change this value by setting the `totp_max_attempts` in the core config. -- A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise even value TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. +- A status of `LIMIT_REACHED_ERROR` indicates that the user has entered an incorrect TOTP too many times and must wait before trying again (otherwise valid TOTPs will fail). The waiting period is indicated by the `retryAfterMs` prop in the response body. By default, it is 15 minutes, but it can be changed by setting the value for `totp_rate_limit_cooldown_sec` in the core config. - A status of `UNKNOWN_DEVICE_ERROR` is possible due to a race condition in which the device is somehow deleted before the verification call is made. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. -On successful verification of a device, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). +On successful verification of a device, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). #### Case 2 implementation: User needs to complete the TOTP challenge This case is when the user already has a device setup (`totp` is in `factorsThatAreAlreadySetup`), and needs to complete the TOTP challenge. In this case, you should show the user an input box asking them to enter their TOTP from the authenticator app and then call the following API: - - - - - + + +The above API call returns the following response: + + + + + + ```tsx -import Totp from "supertokens-web-js/recipe/totp" -import Session from "supertokens-web-js/recipe/session" +import Totp from "supertokens-web-js/recipe/totp"; +import Session from "supertokens-web-js/recipe/session"; async function verifyTotpCode(userInputTotp: string) { - if (await Session.doesSessionExist()) { - try { - let verifyResponse = await Totp.verifyCode({ - totp: userInputTotp, - }); - if (verifyResponse.status === "LIMIT_REACHED_ERROR") { - // this can happen if the user has entered a wrong TOTP too many times. - window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); - } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { - window.alert("Totp incorrect. Please try again"); - } else { - // Code verified successfully - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") + if (await Session.doesSessionExist()) { + try { + let verifyResponse = await Totp.verifyCode({ + totp: userInputTotp, + }); + if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Code verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP code verification can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - -```tsx -import supertokensTotp from "supertokens-web-js-script/recipe/totp" -import supertokensSession from "supertokens-web-js-script/recipe/session" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function verifyTotpCode(userInputTotp: string) { - if (await supertokensSession.doesSessionExist()) { - try { - let verifyResponse = await supertokensTotp.verifyCode({ - totp: userInputTotp, - }); - if (verifyResponse.status === "LIMIT_REACHED_ERROR") { - // this can happen if the user has entered a wrong TOTP too many times. - window.alert("Totp incorrect. Please try again in " + (verifyResponse.retryAfterMs / 1000) + " seconds"); - } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { - window.alert("Totp incorrect. Please try again"); - } else { - // Code verified successfully - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("TOTP code verification can only happen after the first factor is complete and when a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + let verifyResponse = await supertokensTotp.verifyCode({ + totp: userInputTotp, + }); + if (verifyResponse.status === "LIMIT_REACHED_ERROR") { + // this can happen if the user has entered a wrong TOTP too many times. + window.alert("Totp incorrect. Please try again in " + verifyResponse.retryAfterMs / 1000 + " seconds"); + } else if (verifyResponse.status === "INVALID_TOTP_ERROR") { + window.alert("Totp incorrect. Please try again"); + } else { + // Code verified successfully + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error( + "TOTP code verification can only happen after the first factor is complete and when a session exists", + ); + } } ``` - - - - - - - - - - - -The above API call returns the following response: - -```json + + + + +```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK" | "UNKNOWN_USER_ID_ERROR" } | { @@ -809,10 +742,10 @@ The above API call returns the following response: "status": "GENERAL_ERROR" } ``` + + - - - A `status: OK` indicates that verification was successful. SuperTokens tries and verifies the input TOTP against all verified devices that belong to this user. - A status of `INVALID_TOTP_ERROR` means that the user has entered the an incorrect TOTP and needs to retry. The response contains two other props: @@ -822,68 +755,66 @@ The above API call returns the following response: - A status of `UNKNOWN_USER_ID_ERROR` is possible due to a race condition in which all devices that the user had are deleted by the time this API is called. In this case, you can ask users to setup a new device. - A status of `GENERAL_ERROR` is possible if you specifically return that from a backend API override. -On successful verification of the code, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](../frontend-setup#mfa-info-endpoint). +On successful verification of the code, the `totp` factor is marked as completed and the `v` value is updated in the session based on if there are any more factors that the user needs to complete. The next step would be to check this `v` value in the MFA claim and redirect the user to the application page, or get information about the next factor using the [MFA info endpoint](/additional-verification/mfa/initial-setup#the-mfa-info-endpoint). - + -
+ - + -In a multi tenancy setup, you may want to enable TOTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling TOTP for all users within specific tenants. +In a multi tenancy setup, you may want to enable TOTP for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#1-configure-the-backend) section above, so in this section, we will focus on enabling TOTP for all users within specific tenants. ### 1. Configure the backend To start, we will initialise the TOTP and the MultiFactorAuth recipes in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - totp.init(), - MultiFactorAuth.init() - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init(), + MultiFactorAuth.init(), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp @@ -897,63 +828,54 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ totp.init(), multifactorauth.init(), ], ) ``` - - - + + Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. - - - - + + To configure TOTP requirement for a tenant, we can call the following API: + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - requiredSecondaryFactors: [MultiFactorAuth.FactorIds.TOTP] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + requiredSecondaryFactors: [MultiFactorAuth.FactorIds.TOTP], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } } ``` + + -- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. -- We set the `requiredSecondaryFactors` to `["totp"]` to indicate that TOTP is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. - - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -963,7 +885,7 @@ from supertokens_python.recipe.multifactorauth.types import FactorIds async def create_new_tenant(): resp = await create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( - first_factors=[FactorIds.EMAILPASSWORD], + first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.TOTP], ) ) @@ -975,10 +897,8 @@ async def create_new_tenant(): # Existing tenant's config was modified pass ``` - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate @@ -988,7 +908,7 @@ from supertokens_python.recipe.multifactorauth.types import FactorIds def create_new_tenant(): resp = create_or_update_tenant( "customer1", TenantConfigCreateOrUpdate( - first_factors=[FactorIds.EMAILPASSWORD], + first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], required_secondary_factors=[FactorIds.TOTP], ) ) @@ -1000,23 +920,27 @@ def create_new_tenant(): # Existing tenant's config was modified pass ``` + + + + - - - - - - + + +- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. +- We set the `requiredSecondaryFactors` to `["totp"]` to indicate that TOTP is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. + + Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -1024,13 +948,13 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "totp": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } } ``` @@ -1040,120 +964,106 @@ Indicating that the user has finished all required factors, and should be allowe ### 2. Configure the frontend - + We start by modifying the `init` function call on the frontend like so: - + + +You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - +This change is in your auth route config. + + + + ```tsx -import supertokens from "supertokens-auth-react" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import totp from "supertokens-auth-react/recipe/totp" -import Multitenancy from "supertokens-auth-react/recipe/multitenancy" +import supertokens from "supertokens-auth-react"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import totp from "supertokens-auth-react/recipe/totp"; +import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - // other recipes... - // highlight-start - totp.init(), - MultiFactorAuth.init(), - Multitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + // other recipes... + totp.init(), + MultiFactorAuth.init(), + Multitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); ``` - - - - - -You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - -This change is in your auth route config. - -```tsx + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUITOTP from "supertokens-auth-react-script/recipe/totp" -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy" supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - // other recipes... - // highlight-start - supertokensUITOTP.init(), - supertokensUIMultiFactorAuth.init(), - supertokensUIMultitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + // other recipes... + supertokensUITOTP.init(), + supertokensUIMultiFactorAuth.init(), + supertokensUIMultitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); ``` + + + + This change goes in the `supertokens-web-js` SDK config at the root of your application: + + -```tsx -import supertokens from "supertokens-web-js-script"; -import MultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import Session from "supertokens-web-js-script/recipe/session"; - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // highlight-start - Session.init(), - MultiFactorAuth.init(), - // highlight-end - ], + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [Session.init(), MultiFactorAuth.init()], }); ``` - - - - + + - Just like on the backend, we init the `totp` recipe in the `recipeList`. - We also init the `MultiFactorAuth` recipe. Notice that unlike the single tenant setup, we do not specify the `firstFactors` here. That information is fetched based on the tenantId you provide the SDK with. @@ -1162,14 +1072,17 @@ supertokens.init({ Next, we need to add the TOTP pre-built UI when rendering the SuperTokens component: - - - - - - + + +:::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] +::: + + + + + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; @@ -1184,9 +1097,10 @@ function App() {
- // highlight-start - {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-end + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ + /* ... */ TOTPPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} // ... other routes
@@ -1196,11 +1110,8 @@ function App() { ); } ``` - -
- - - + + ```tsx import { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; @@ -1208,45 +1119,30 @@ import { TOTPPreBuiltUI } from "supertokens-auth-react/recipe/totp/prebuiltui"; import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; function App() { - // highlight-start if (canHandleRoute([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI])) { - return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]) + return getRoutingComponent([/* ... */ TOTPPreBuiltUI, MultiFactorAuthPreBuiltUI]); } - // highlight-end - return ( - {/*Your app*/} - ); + return {/*Your app*/}; } ``` + + + + - - -
- -
- - - -:::success -This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. -::: - - - -
+ + With the above configuration, users will see the first and second factor based on the tenant configuration. For the tenant we configured above, users will see email password or social login first. After completing that, users will be redirected to `/auth/mfa/totp` (assuming that the `websiteBasePath` is `/auth`) where they will be asked to setup the factor, or complete the TOTP challenge if they have already setup the factor before. The UI for this screen looks like: - [Factor Setup UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) - [Verification UI](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--verification-with-single-next-option) (In case the factor is already setup before). -
- - - -The steps here are the same as in [the single tenant setup above](#frontend-setup). +
- + -
+The steps here are the same as in [the single tenant setup above](#2-configure-the-frontend). + + diff --git a/docs/additional-verification/mfa/totp/totp-for-opt-in-users.mdx b/docs/additional-verification/mfa/totp/totp-for-opt-in-users.mdx index 10200bd907..d77deb3374 100644 --- a/docs/additional-verification/mfa/totp/totp-for-opt-in-users.mdx +++ b/docs/additional-verification/mfa/totp/totp-for-opt-in-users.mdx @@ -1,21 +1,10 @@ --- title: Require TOTP for specific users -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 2 -description: >- - Implement a TOTP-based MFA policy for specific users based on customizable - criteria. -page_type: guide -recipe: mfa -category: multi-factor-authentication +description: Implement a TOTP-based MFA policy for specific users based on customizable criteria. +sidebar: + order: 2 --- - - -# Require TOTP for specific users - - ## Overview In this page, we will show you how to implement an MFA policy that requires certain users to do TOTP. You can decide which those users are based on any criteria. For example: @@ -33,9 +22,9 @@ The tutorial assumes that the first factor is email password or social login, bu ## Steps - + - + ### 1. Configure the backend @@ -43,79 +32,73 @@ The tutorial assumes that the first factor is email password or social login, bu To start with, we configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" -import UserRoles from "supertokens-node/recipe/userroles" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - UserRoles.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-next-line - totp.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) - if (roles.roles.includes("admin")) { - // we only want totp for admins - return [MultiFactorAuth.FactorIds.TOTP] - } else { - // no MFA for non admin users. - return [] - } - } - } - } - } - // highlight-end - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); + if (roles.roles.includes("admin")) { + // we only want totp for admins + return [MultiFactorAuth.FactorIds.TOTP]; + } else { + // no MFA for non admin users. + return []; + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( @@ -144,7 +127,7 @@ def override_functions(original_implementation: RecipeInterface): roles = await get_roles_for_user(tenant_id, (await user()).id) if "admin" in roles.roles: - # We only want OTP_EMAIL for admins + # We only want TOTP for admins return [FactorIds.TOTP] else: # No MFA for non-admin users @@ -165,7 +148,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ totp.init(), multifactorauth.init( @@ -175,9 +158,8 @@ init( ], ) ``` - - - + + We override the `getMFARequirementsForAuth` function to indicate that `totp` must be completed only for users that have the `admin` role. You can also have any other criteria here. @@ -186,77 +168,75 @@ We override the `getMFARequirementsForAuth` function to indicate that `totp` mus To start with, we configure the backend in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; -import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - totp.init({ - override: { - apis: (oI) => { - return { - ...oI, - verifyDevicePOST: async function (input) { - let response = await oI.verifyDevicePOST!(input); - if (response.status === "OK") { - // device successfully verified. We save that this user has enabled TOTP in the user metadata. - // The multifactorauth recipe will pick this value up next time the user is trying to login, and - // ask them to enter the TOTP code. - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); - } - return response; - } - } - } - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - }) - // highlight-end - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init({ + override: { + apis: (oI) => { + return { + ...oI, + verifyDevicePOST: async function (input) { + let response = await oI.verifyDevicePOST!(input); + if (response.status === "OK") { + // device successfully verified. We save that this user has enabled TOTP in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the TOTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( + input.session.getUserId(), + MultiFactorAuth.FactorIds.TOTP, + ); + } + return response; + }, + }; + }, + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( @@ -308,7 +288,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( @@ -317,9 +297,8 @@ init( ], ) ``` - - - + + We initialise the multi factor auth recipe here without any override to `getMFARequirementsForAuth`. The default implementation of this function already checks what factors are enabled for a user and returns those. Therefore all we need to do is mark `totp` as enabled for a user as soon as they have setup a device successfully. This happens in the `verifyDevicePOST` API override as shown above. Once a device is verified, we mark the `totp` factor as enabled for the user, and the next time they login, they will be asked to complete the TOTP challenge. @@ -332,12 +311,12 @@ In both of the examples above, notice that we have initialised the TOTP recipe i Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this (for those that require TOTP): ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } } ``` @@ -345,13 +324,13 @@ The `v` being `false` indicates that there are still factors that are pending. A ```json { - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "totp": 1702877999 - }, - "v": true - } + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "totp": 1702877999 + }, + "v": true + } } ``` @@ -363,7 +342,7 @@ Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). -The first part is identical to the steps mentioned in [this section](./totp-for-all-users#frontend-setup), please follow that. +The first part is identical to the steps in [Configure the frontend](/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-frontend). The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. @@ -372,31 +351,30 @@ The second part, which is only applicable in case you want to allow users to ena In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { - let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) - return factors.includes(MultiFactorAuth.FactorIds.TOTP) + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); + return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -405,10 +383,8 @@ async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -417,43 +393,40 @@ def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` - - - - - - + + + + If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } async function disableMFAForUser(userId: string) { - await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) + await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, @@ -468,10 +441,8 @@ async def enable_mfa_for_user(user_id: str) -> None: async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, @@ -480,107 +451,103 @@ from supertokens_python.recipe.multifactorauth.syncio import ( from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: - add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) + add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) def disable_mfa_for_user(user_id: str) -> None: - remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) + remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` - - - - - - + + + + In order to list existing TOTP devices on the frontend, you can call the following API: - - - - + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + + + + + + ```tsx -import Session from "supertokens-web-js/recipe/session" -import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session"; +import Totp from "supertokens-web-js/recipe/totp"; async function fetchTOTPDevices() { - if (await Session.doesSessionExist()) { - try { - let totpDevicesResponse = await Totp.listDevices(); - for (let i = 0; i < totpDevicesResponse.devices.length; i++) { - let currDevice = totpDevicesResponse.devices[i]; - console.log(currDevice.name) // by default, this will be like "TOTP Device 1" - console.log(currDevice.verified) - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await Session.doesSessionExist()) { + try { + let totpDevicesResponse = await Totp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name); // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session" -import supertokensTotp from "supertokens-web-js-script/recipe/totp" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchTOTPDevices() { - if (await supertokensSession.doesSessionExist()) { - try { - let totpDevicesResponse = await supertokensTotp.listDevices(); - for (let i = 0; i < totpDevicesResponse.devices.length; i++) { - let currDevice = totpDevicesResponse.devices[i]; - console.log(currDevice.name) // by default, this will be like "TOTP Device 1" - console.log(currDevice.verified) - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + let totpDevicesResponse = await supertokensTotp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name); // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - - - - - - - -Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): - + + + + ```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/totp/device/list' \ +curl --location --request GET '/auth/totp/device/list' \ --header 'Authorization: Bearer ...' ``` + + + + The output from the API call is as follows: + + -```json + + +```json check=false reason="response type excerpt uses array and union notation" { "status": "OK", "devices": { @@ -593,102 +560,99 @@ The output from the API call is as follows: "status": "GENERAL_ERROR" } ``` + + - - - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: - - - - - + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + + + + + + ```tsx -import Session from "supertokens-web-js/recipe/session" -import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session"; +import Totp from "supertokens-web-js/recipe/totp"; async function removeTOTPDevices(deviceName: string) { - if (await Session.doesSessionExist()) { - try { - await Totp.removeDevice({ - deviceName - }); - // device is removed - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await Session.doesSessionExist()) { + try { + await Totp.removeDevice({ + deviceName, + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session" -import supertokensTotp from "supertokens-web-js-script/recipe/totp" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function removeTOTPDevices(deviceName: string) { - if (await supertokensSession.doesSessionExist()) { - try { - await supertokensTotp.removeDevice({ - deviceName - }); - // device is removed - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + await supertokensTotp.removeDevice({ + deviceName, + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - - - - - - - -Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/totp/device/remove' \ +curl --location --request POST '/auth/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` + + + + The output from the API call is as follows: + + -```json + + +```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "didDeviceExist": true; @@ -696,143 +660,146 @@ The output from the API call is as follows: "status": "GENERAL_ERROR" } ``` + + - - - + - - - - + + + In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: + + +In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: + +- We add the query param of `setup=true` because we want to create a new device. +- The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. + + + + ```tsx -import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth'; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { - MultiFactorAuth.redirectToFactor({ - factorId: "totp", - forceSetup: true, - redirectBack: true, - }) + MultiFactorAuth.redirectToFactor({ + factorId: "totp", + forceSetup: true, + redirectBack: true, + }); } ``` + + + + + + + - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. + + +After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#1-configure-the-backend) above) will add TOTP as a required factor for this user, ensuring that next time they login, they will be asked to complete the TOTP challenge. - - - - -In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: -- We add the query param of `setup=true` because we want to create a new device. -- The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. - - - - - -After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#backend-setup) above) will add TOTP as a required factor for this user, ensuring that next time they login, they will be asked to complete the TOTP challenge. + - + - +To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in [Case 1: Set up a new TOTP device](/additional-verification/mfa/totp/totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device). -In order to create a new device, you should redirect the user to a page which creates a new TOTP device on the backend, asks the user to scan the QR code and then to enter the TOTP in order to verify the new device. This can be achieved by calling the functions mentioned in [this section](./totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device--cust). + - + - - - + ### 1. Configure the backend -A user can be a part of multiple tenants. If you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#backend-setup) section above. +A user can be a part of multiple tenants. If you want TOTP to be enabled for a specific user across all the tenants that they are a part of, the steps are the same as in the [Backend setup](#1-configure-the-backend) section above. -However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#backend-setup) section above: +However, if you want TOTP to be enabled for a specific user, for a specific tenant (or a sub set of tenants that the user is a part of), then you will have to add additional logic to the `getMFARequirementsForAuth` function override. Modifying the example code from the [Backend setup](#1-configure-the-backend) section above: #### Only enable TOTP for users that have an `admin` role - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" -import UserRoles from "supertokens-node/recipe/userroles" +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - UserRoles.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-next-line - totp.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id) - // highlight-next-line - if (roles.roles.includes("admin") && (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { - // we only want totp for admins - return [MultiFactorAuth.FactorIds.TOTP] - } else { - // no MFA for non admin users. - return [] - } - } - } - } - } - }) - ] -}) + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + UserRoles.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + let roles = await UserRoles.getRolesForUser(input.tenantId, (await input.user).id); + if ( + roles.roles.includes("admin") && + (await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP) + ) { + // we only want totp for admins + return [MultiFactorAuth.FactorIds.TOTP]; + } else { + // no MFA for non admin users. + return []; + } + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( @@ -864,7 +831,7 @@ def override_functions(original_implementation: RecipeInterface): "admin" in roles.roles and FactorIds.TOTP in await required_secondary_factors_for_tenant() ): - # We only want OTP_EMAIL for admins + # We only want TOTP for admins return [FactorIds.TOTP] else: # No MFA for non-admin users @@ -885,7 +852,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ multifactorauth.init( first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], @@ -895,104 +862,100 @@ init( ], ) ``` + + - - - -- The implementation of `shouldRequireTotpForTenant` is entirely up to you. - +- The override checks `requiredSecondaryFactorsForTenant` (Python: `required_secondary_factors_for_tenant`) so TOTP is required only when the tenant configuration includes it. #### Ask for TOTP only for users that have enabled TOTP on their account - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; -import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth" -import totp from "supertokens-node/recipe/totp" -import Session from "supertokens-node/recipe/session" +import MultiFactorAuth, { MultiFactorAuthClaim } from "supertokens-node/recipe/multifactorauth"; +import totp from "supertokens-node/recipe/totp"; +import Session from "supertokens-node/recipe/session"; supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - totp.init({ - override: { - apis: (oI) => { - return { - ...oI, - verifyDevicePOST: async function (input) { - let response = await oI.verifyDevicePOST!(input); - if (response.status === "OK") { - // device successfully verified. We save that this user has enabled TOTP in the user metadata. - // The multifactorauth recipe will pick this value up next time the user is trying to login, and - // ask them to enter the TOTP code. - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(input.session.getUserId(), MultiFactorAuth.FactorIds.TOTP); - } - return response; - } - } - } - } - }), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { - // this means that the user has finished setting up a device from their settings page. - if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { - return [MultiFactorAuth.FactorIds.TOTP] - } - } - // no totp required for input.user, with the input.tenant. - return [] - } - } + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + totp.init({ + override: { + apis: (oI) => { + return { + ...oI, + verifyDevicePOST: async function (input) { + let response = await oI.verifyDevicePOST!(input); + if (response.status === "OK") { + // device successfully verified. We save that this user has enabled TOTP in the user metadata. + // The multifactorauth recipe will pick this value up next time the user is trying to login, and + // ask them to enter the TOTP code. + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser( + input.session.getUserId(), + MultiFactorAuth.FactorIds.TOTP, + ); + } + return response; + }, + }; + }, + }, + }), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + if ((await input.requiredSecondaryFactorsForUser).includes(MultiFactorAuth.FactorIds.TOTP)) { + // this means that the user has finished setting up a device from their settings page. + if ((await input.requiredSecondaryFactorsForTenant).includes(MultiFactorAuth.FactorIds.TOTP)) { + return [MultiFactorAuth.FactorIds.TOTP]; } - } - // highlight-end - }) - ] -}) + } + // no totp required for input.user, with the input.tenant. + return []; + }, + }; + }, + }, + }), + ], +}); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="framework placeholder must be replaced for the target Python server" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multifactorauth, totp from supertokens_python.recipe.multifactorauth.types import ( @@ -1070,7 +1033,7 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework="...", # type: ignore + framework="...", recipe_list=[ totp.init(TOTPConfig(override=OverrideConfig(apis=totp_override))), multifactorauth.init( @@ -1080,11 +1043,10 @@ init( ], ) ``` + + - - - -- We provide an override for `getMFARequirementsForAuth` which checks if TOTP is enabled for the user, and also take into account the tenantId to decide if we want to have this user go through the TOTP flow whilst logging into this tenant. The implementation of `shouldRequireTotpForTenant` is entirely up to you. +- The `getMFARequirementsForAuth` override checks both the user's required factors and `requiredSecondaryFactorsForTenant` (Python: `required_secondary_factors_for_tenant`). TOTP is required only when it is enabled for that user and allowed by the current tenant configuration. ### 2. Configure the frontend @@ -1093,7 +1055,7 @@ Two parts exist to this: - Configuring the frontend to show the TOTP UI when required during login / sign up - Allowing users to enable / disable TOTP on their account via the settings page (If you are following Example 2 from above). -The first part is identical to the steps mentioned in [this section](/docs/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-), so please follow that. +The first part is identical to [Configure the frontend](/additional-verification/mfa/totp/totp-for-all-users#2-configure-the-frontend). The second part, which is only applicable in case you want to allow users to enable / disable TOTP themselves, can be achieved by creating the following flow on your frontend: - When the user navigates to their settings page, you can show them if TOTP is enabled or not. @@ -1102,31 +1064,30 @@ The second part, which is only applicable in case you want to allow users to ena In order to know if the user has enabled TOTP, you can make an API your backend which calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function isTotpEnabledForUser(userId: string) { - let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId) - return factors.includes(MultiFactorAuth.FactorIds.TOTP) + let factors = await MultiFactorAuth.getRequiredSecondaryFactorsForUser(userId); + return factors.includes(MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1135,10 +1096,8 @@ async def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = await get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import get_required_secondary_factors_for_user from supertokens_python.recipe.multifactorauth.types import FactorIds @@ -1147,43 +1106,40 @@ def is_totp_factor_enabled_for_user(user_id: str) -> bool: factors = get_required_secondary_factors_for_user(user_id, {}) return FactorIds.TOTP in factors ``` - - - - - - + + + + If the user wants to enable or disable TOTP for them, you can make an API on your backend which calls the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```ts import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; async function enableMFAForUser(userId: string) { - await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) + await MultiFactorAuth.addToRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } async function disableMFAForUser(userId: string) { - await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP) + await MultiFactorAuth.removeFromRequiredSecondaryFactorsForUser(userId, MultiFactorAuth.FactorIds.TOTP); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.multifactorauth.asyncio import ( add_to_required_secondary_factors_for_user, @@ -1198,10 +1154,8 @@ async def enable_mfa_for_user(user_id: str) -> None: async def disable_mfa_for_user(user_id: str) -> None: await remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` - - - - + + ```python from supertokens_python.recipe.multifactorauth.syncio import ( add_to_required_secondary_factors_for_user, @@ -1210,107 +1164,102 @@ from supertokens_python.recipe.multifactorauth.syncio import ( from supertokens_python.recipe.multifactorauth.types import FactorIds def enable_mfa_for_user(user_id: str) -> None: - add_to_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) + add_to_required_secondary_factors_for_user(user_id, FactorIds.TOTP) def disable_mfa_for_user(user_id: str) -> None: - remove_from_required_secondary_factors_for_user(user_id, FactorIds.OTP_EMAIL) + remove_from_required_secondary_factors_for_user(user_id, FactorIds.TOTP) ``` - - - - - - + + + + In order to list existing TOTP devices on the frontend, you can call the following API: - - - - - + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + + + + + + ```tsx -import Session from "supertokens-web-js/recipe/session" -import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session"; +import Totp from "supertokens-web-js/recipe/totp"; async function fetchTOTPDevices() { - if (await Session.doesSessionExist()) { - try { - let totpDevicesResponse = await Totp.listDevices(); - for (let i = 0; i < totpDevicesResponse.devices.length; i++) { - let currDevice = totpDevicesResponse.devices[i]; - console.log(currDevice.name) // by default, this will be like "TOTP Device 1" - console.log(currDevice.verified) - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await Session.doesSessionExist()) { + try { + let totpDevicesResponse = await Totp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name); // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session" -import supertokensTotp from "supertokens-web-js-script/recipe/totp" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function fetchTOTPDevices() { - if (await supertokensSession.doesSessionExist()) { - try { - let totpDevicesResponse = await supertokensTotp.listDevices(); - for (let i = 0; i < totpDevicesResponse.devices.length; i++) { - let currDevice = totpDevicesResponse.devices[i]; - console.log(currDevice.name) // by default, this will be like "TOTP Device 1" - console.log(currDevice.verified) - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + let totpDevicesResponse = await supertokensTotp.listDevices(); + for (let i = 0; i < totpDevicesResponse.devices.length; i++) { + let currDevice = totpDevicesResponse.devices[i]; + console.log(currDevice.name); // by default, this will be like "TOTP Device 1" + console.log(currDevice.verified); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - - - - - - - -Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): - + + + + ```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/totp/device/list' \ +curl --location --request GET '/auth/totp/device/list' \ --header 'Authorization: Bearer ...' ``` + + + + The output from the API call is as follows: + + -```json + + +```json check=false reason="response type excerpt uses array and union notation" { "status": "OK", "devices": { @@ -1323,102 +1272,99 @@ The output from the API call is as follows: "status": "GENERAL_ERROR" } ``` + + - - - A `status: OK` will contain the list of all devices that exist for this user, across all of the user's tenants. We recommend only showing the devices that are `verified` to the user. - A `status: GENERAL_ERROR`: This is possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend In order to remove a device, you can call the following API from the frontend: - - - - - + + +Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): + + + + + + ```tsx -import Session from "supertokens-web-js/recipe/session" -import Totp from "supertokens-web-js/recipe/totp" +import Session from "supertokens-web-js/recipe/session"; +import Totp from "supertokens-web-js/recipe/totp"; async function removeTOTPDevices(deviceName: string) { - if (await Session.doesSessionExist()) { - try { - await Totp.removeDevice({ - deviceName - }); - // device is removed - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await Session.doesSessionExist()) { + try { + await Totp.removeDevice({ + deviceName, + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session" -import supertokensTotp from "supertokens-web-js-script/recipe/totp" - + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function removeTOTPDevices(deviceName: string) { - if (await supertokensSession.doesSessionExist()) { - try { - await supertokensTotp.removeDevice({ - deviceName - }); - // device is removed - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } - } else { - throw new Error("Illegal function call: Please only call this function if a session exists") + if (await supertokensSession.doesSessionExist()) { + try { + await supertokensTotp.removeDevice({ + deviceName, + }); + // device is removed + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } } + } else { + throw new Error("Illegal function call: Please only call this function if a session exists"); + } } ``` - - - - - - - - - - - -Notice that the API call requires the session's access token as an input (this should be added by our frontend SDK automatically): - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/totp/device/remove' \ +curl --location --request POST '/auth/totp/device/remove' \ --header 'Authorization: Bearer ...' --header 'Content-Type: application/json' \ --data-raw '{ "deviceName": "..." }' ``` + + + + The output from the API call is as follows: + + -```json + + +```json check=false reason="response alternatives are shown as a JSON-like union" { "status": "OK", "didDeviceExist": true; @@ -1426,57 +1372,61 @@ The output from the API call is as follows: "status": "GENERAL_ERROR" } ``` + + - - - - - + - - + + + In order to add a new device, you can call the following function from the frontend. This function will redirect the user to the TOTP create device pre-built UI. After the user has finished the new device creation and verification, they will be redirected back to the current page: + + +In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: + +- We add the query param of `setup=true` because we want to create a new device. +- The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. + + + + ```tsx -import MultiFactorAuth from 'supertokens-auth-react/recipe/multifactorauth'; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; async function redirectToTotpSetupScreen() { - MultiFactorAuth.redirectToFactor({ - factorId: "totp", - forceSetup: true, - redirectBack: true, - }) + MultiFactorAuth.redirectToFactor({ + factorId: "totp", + forceSetup: true, + redirectBack: true, + }); } ``` + + + + + + + - In the snippet above, we redirect to the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option). We set the `forceSetup` to `true` since we want the user to setup a new TOTP device. The `redirectBack` boolean is also `true` since we want to redirect back to the current page after the user has finished setting up the device. - You can also redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` if you don't want to use the above function. - - - - - - -In order to add a new device, you can redirect the user to `/{websiteBasePath}/mfa/totp?setup=true&redirectToPath={currentPath}` from your settings page. This will show the [TOTP factor setup screen](https://6571be2867f75556541fde98-xieqfaxuuo.chromatic.com/?path=/story/totp-mfa--device-setup-with-single-next-option) to the user: -- We add the query param of `setup=true` because we want to create a new device. -- The `redirectToPath` query param will also tell our SDK to redirect the user back to the current page after they have finished creating the device. - - - - + + After the user has finished creating a device, our backend override for `verifyDevicePOST` (see "Example 2" in [Backend setup section](#1-configure-the-backend) above) will add TOTP as a required factor for this user, so that next time they login, they will be asked to complete the TOTP challenge. - - - + -In order to create a new device, you should redirect the user to a page which creates a new TOTP device on the backend, asks the user to scan the QR code and then to enter the TOTP in order to verify the new device. This can be achieved by calling the functions mentioned in [this section](./totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device--cust). + - +To create a new device, redirect the user to a page that creates a TOTP device on the backend, asks the user to scan the QR code, and then verifies a TOTP. Use the functions in [Case 1: Set up a new TOTP device](/additional-verification/mfa/totp/totp-for-all-users#case-1-implementation-user-needs-to-setup-a-new-totp-device). - + + diff --git a/docs/additional-verification/mfa/webauthn-secondary-factor-setup.mdx b/docs/additional-verification/mfa/webauthn-secondary-factor-setup.mdx deleted file mode 100644 index 425f037735..0000000000 --- a/docs/additional-verification/mfa/webauthn-secondary-factor-setup.mdx +++ /dev/null @@ -1,903 +0,0 @@ ---- -id: webauthn-setup -title: Passkeys/WebAuthn -hide_title: true -sidebar_position: 6.2 -description: >- - Implement WebAuthn/Passkey multi-factor authentication that requires all users to complete passkey authentication before accessing the application. -page_type: guide -recipe: mfa -category: multi-factor-authentication ---- - -# Implement WebAuthn as a secondary factor - - -## Overview - -This guide shows how to implement an MFA policy that requires all users to use WebAuthn before they get access to your application. - -## Before you start - -The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. - - - -## Steps - - - -### 1. Configure the backend - - - -To start with, we configure the backend in the following way: - - - - -```ts -import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import webauthn from "supertokens-node/recipe/webauthn" -import Session from "supertokens-node/recipe/session" - -supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-next-line - webauthn.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getMFARequirementsForAuth: async function (input) { - // Change this implementation if you want to require webauthn only for specific users - return [MultiFactorAuth.FactorIds.WEBAUTHN] - } - } - } - } - // highlight-end - }) - ] -}) -``` - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, webauthn -from supertokens_python.recipe.multifactorauth.types import ( - FactorIds, - OverrideConfig, - MFARequirementList, -) -from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface -from supertokens_python.types import User -from typing import Dict, Any, Callable, Awaitable, List - - -def override_functions(original_implementation: RecipeInterface): - async def get_mfa_requirements_for_auth( - tenant_id: str, - access_token_payload: Dict[str, Any], - completed_factors: Dict[str, int], - user: Callable[[], Awaitable[User]], - factors_set_up_for_user: Callable[[], Awaitable[List[str]]], - required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], - required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], - user_context: Dict[str, Any], - ) -> MFARequirementList: - # Change this implementation if you want to require webauthn only for specific users - return [FactorIds.WEBAUTHN] - - original_implementation.get_mfa_requirements_for_auth = ( - get_mfa_requirements_for_auth - ) - return original_implementation - - -init( - app_info=InputAppInfo( - app_name="...", - api_domain="...", - website_domain="...", - ), - supertokens_config=SupertokensConfig( - connection_uri="...", - ), - framework="...", # type: ignore - recipe_list=[ - webauthn.init(), - multifactorauth.init( - first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], - override=OverrideConfig(functions=override_functions), - ), - ], -) -``` - - - - -The MFA recipe override is required to indicate that `webauthn` must be completed before the user can access the app. - -Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: -```json -{ - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } -} -``` - -The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: - -```json -{ - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "webauthn": 1702877999 - }, - "v": true - } -} -``` - -Indicating that the user has finished all required factors, and should be allowed to access the app. - - - - - - -In a multi tenancy setup, you may want to enable WebAuthn for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#single-tenant-setup) section above, so in this section, we will focus on enabling WebAuthn for all users within specific tenants. - -To start, we will initialise the WebAuthn and the MultiFactorAuth recipes in the following way: - - - - -```ts -import supertokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" -import webauthn from "supertokens-node/recipe/webauthn" -import Session from "supertokens-node/recipe/session" - -supertokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init(), - ThirdParty.init({ - //... - }), - EmailPassword.init({ - //... - }), - // highlight-start - webauthn.init(), - MultiFactorAuth.init() - // highlight-end - ] -}) -``` - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import multifactorauth, webauthn - - -init( - app_info=InputAppInfo( - app_name="...", - api_domain="...", - website_domain="...", - ), - supertokens_config=SupertokensConfig( - connection_uri="...", - ), - framework="...", # type: ignore - recipe_list=[ - webauthn.init(), - multifactorauth.init(), - ], -) -``` - - - - -Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. - - - - - -To configure WebAuthn requirement for a tenant, we can call the following API: - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; -import MultiFactorAuth from "supertokens-node/recipe/multifactorauth" - -async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ], - requiredSecondaryFactors: [MultiFactorAuth.FactorIds.WEBAUTHN] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } -} -``` - -- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. -- We set the `requiredSecondaryFactors` to `["webauthn"]` to indicate that WebAuthn is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. - - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate -from supertokens_python.recipe.multifactorauth.types import FactorIds - - -async def create_new_tenant(): - resp = await create_or_update_tenant( - "customer1", TenantConfigCreateOrUpdate( - first_factors=[FactorIds.EMAILPASSWORD], - required_secondary_factors=[FactorIds.WEBAUTHN], - ) - ) - - if resp.created_new: - # Tenant created successfully - pass - else: - # Existing tenant's config was modified - pass -``` - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate -from supertokens_python.recipe.multifactorauth.types import FactorIds - - -def create_new_tenant(): - resp = create_or_update_tenant( - "customer1", TenantConfigCreateOrUpdate( - first_factors=[FactorIds.EMAILPASSWORD], - required_secondary_factors=[FactorIds.WEBAUTHN], - ) - ) - - if resp.created_new: - # Tenant created successfully - pass - else: - # Existing tenant's config was modified - pass -``` - - - - - - - - -Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: -```json -{ - "st-mfa": { - "c": { - "emailpassword": 1702877939, - }, - "v": false - } -} -``` - -The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: - -```json -{ - "st-mfa": { - "c": { - "emailpassword": 1702877939, - "webauthn": 1702877999 - }, - "v": true - } -} -``` - -Indicating that the user has finished all required factors, and should be allowed to access the app. - - - -### 2. Configure the frontend - - - - - -We start by modifying the `init` function call on the frontend like so: - - - - - - - -```tsx -import supertokens from "supertokens-auth-react" -import Multitenancy from "supertokens-auth-react/recipe/multitenancy" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import webauthn from "supertokens-auth-react/recipe/webauthn" - -supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - webauthn.init(), - MultiFactorAuth.init(), - Multitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) -``` - - - - - -You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - -This change is in your auth route config. - -```tsx -// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUIWebAuthn from "supertokens-auth-react-script/recipe/webauthn" -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy" - -supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - supertokensUIWebAuthn.init(), - supertokensUIMultiFactorAuth.init(), - supertokensUIMultitenancy.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - getTenantId: async (context) => { - return "TODO" - } - } - } - } - }) - // highlight-end - ] -}) -``` - -This change goes in the `supertokens-web-js` SDK config at the root of your application: - -```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; -import WebAuthn from "supertokens-web-js/recipe/webauthn"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - WebAuthn.init() - // highlight-end - ], -}); -``` - - - - - - - - - - - - - -```tsx -import supertokens from "supertokens-auth-react" -import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth" -import webauthn from "supertokens-auth-react/recipe/webauthn" - -supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - webauthn.init(), - MultiFactorAuth.init({ - firstFactors: [ - MultiFactorAuth.FactorIds.EMAILPASSWORD, - MultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) -``` - - - - - -You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: - -This change is in your auth route config. - -```tsx -// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIMultiFactorAuth from "supertokens-auth-react-script/recipe/multifactorauth" -import supertokensUIWebAuthn from "supertokens-auth-react-script/recipe/webauthn" -supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - // other recipes.. - // highlight-start - supertokensUIWebAuthn.init(), - supertokensUIMultiFactorAuth.init({ - firstFactors: [ - supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, - supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY - ] - }) - // highlight-end - ] -}) -``` - -This change goes in the `supertokens-web-js` SDK config at the root of your application: - -```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; -import WebAuthn from "supertokens-web-js/recipe/webauthn"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - WebAuthn.init() - // highlight-end - ], -}); -``` - - - - - - - - -On the frontend, the `MultiFactorAuth` recipe intialization only requires the first factors to be configured. -The secondary factors will be determined based on a requiest to the backend. - -Add the WebAuthn pre-built UI to render the SuperTokens component: - - - - - - - - -```tsx -import { SuperTokensWrapper } from "supertokens-auth-react"; -import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; -import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; - -function App() { - return ( - -
- -
- - // highlight-start - {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])} - // highlight-end - // ... other routes - -
-
-
-
- ); -} -``` - -
- - - -```tsx -import { SuperTokensWrapper } from "supertokens-auth-react"; -import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; -import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; -import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; - -function App() { - // highlight-start - if (canHandleRoute([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])) { - return getRoutingComponent([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI]) - } - // highlight-end - return ( - {/*Your app*/} - ); -} -``` - - - -
- -
- - - -:::success -This step is not required for non React apps, since all the pre-built UI components are already added into the bundle. -::: - - - -
- -
- - - -We start by initialising the MFA and WebAuthn recipe on the frontend like so: - - - - - - - - - -```tsx -import SuperTokens from 'supertokens-web-js'; -import MultiFactorAuth from 'supertokens-web-js/recipe/multifactorauth'; -import WebAuthn from "supertokens-web-js/recipe/webauthn"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - MultiFactorAuth.init(), - WebAuthn.init() - // highlight-end - ], -}); -``` - - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultiFactorAuth from 'supertokens-web-js-script/recipe/multifactorauth'; -import supertokensWebAuthn from "supertokens-web-js-script/recipe/webauthn"; -supertokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - // other recipes... - // highlight-start - supertokensMultiFactorAuth.init(), - supertokensWebAuthn.init() - // highlight-end - ], -}); -``` - - - - - - - - - - -:::success -This step is not applicable for mobile apps. Please continue reading. -::: - - - - - -After the first factor login, you should start by checking the access token payload and see if the MFA claim's `v` boolean is `false`. -'If it's not, then you can redirect the user to the application page. - -If it's `false`, the frontend then needs to [call the MFA endpoint](/docs/references/fdi/mfa/put-mfa-info) to get information about which factor the user should be asked to complete next. -Based on the initial backend configuration, the `next` array will contain `["webauthn"]`. - -To complete the secondary factor you need to take into account if the users has previously configured a passkey or not. -You can determine this by checking if the `alreadySetup` array contains `"webauthn"`. - - -#### Sign up flow - - - - - - - - - -```ts -import Webauthn from "supertokens-web-js/recipe/webauthn" -import Session from "supertokens-web-js/recipe/session" - -async function secondFactorSignUp(email: string, userContext: Record) { - const response = await Webauthn.registerCredentialWithSignUp({ - email, - shouldTryLinkingWithSessionUser: true, - userContext, - }); - - return response.status === "OK"; -} -``` - - - - - -```ts -import supertokensWebauthn from "supertokens-web-js-script/recipe/webauthn" -import supertokensSession from "supertokens-web-js-script/recipe/session" - -async function secondFactorSignUp(email: string, userContext: Record) { - const response = await recipeImplementation.registerCredentialWithSignUp({ - email, - shouldTryLinkingWithSessionUser: true, - userContext, - }); - - return response.status === "OK"; -} -``` - - - - - - - - - -Support for this flow is not available in the mobile SDK. -You will have to call the [backend API](/docs/references/fdi/introduction) directly. - -First, call the [**Register WebAuthn Credential**](/docs/references/fdi/webauthn/post-webauthn-credential) endpoint to register the passkey. -Afterwards call the [**Sign Up with WebAuthn**](/docs/references/fdi/webauthn/post-webauthn-signup) to complete the second factor sign up process. - - - - - -#### Sign in flow - - - - - - - - -```ts -import Webauthn from "supertokens-web-js/recipe/webauthn" -import Session from "supertokens-web-js/recipe/session" - -async function secondFactorSignUp(userContext: Record) { - const response = await recipeImplementation.authenticateCredentialWithSignIn({ - shouldTryLinkingWithSessionUser: true, - userContext, - }); - - return response.status === "OK"; -} -``` - - - -```ts -import supertokensWebauthn from "supertokens-web-js-script/recipe/webauthn" -import supertokensSession from "supertokens-web-js-script/recipe/session" - -async function secondFactorSignUp(userContext: Record) { - const response = await recipeImplementation.authenticateCredentialWithSignIn({ - shouldTryLinkingWithSessionUser: true, - userContext, - }); - - return response.status === "OK"; -} -``` - - - - - - - - - - -Support for this flow is not available in the mobile SDK. -You will have to call the [backend API](/docs/references/fdi/introduction) directly. - -Call the [**Sign in with WebAuthn**](/docs/references/fdi/webauthn/post-webauthn-signin) endpoint to complete the secondary factor flow. - - - - - - - - - - - -That's it! :tada: - -Based on this configuration, users first access the authentication form which shows the `emailpassword` and `thirdparty` options. -After first factor completion, they access the WebAuthn form to finalize the authentication attempt. diff --git a/docs/additional-verification/mfa/webauthn-setup.mdx b/docs/additional-verification/mfa/webauthn-setup.mdx new file mode 100644 index 0000000000..aecd53c1bd --- /dev/null +++ b/docs/additional-verification/mfa/webauthn-setup.mdx @@ -0,0 +1,1099 @@ +--- +title: Passkeys as an MFA Factor +description: Require WebAuthn passkeys as a second authentication factor after email/password, social login, or another first factor. +sidebar: + order: 60 +--- + +## Overview + +This guide shows how to implement an MFA policy that requires all users to use WebAuthn before they get access to your application. + +For standalone passwordless sign-in with passkeys, use the [Passkey Authentication guide](/authentication/passkeys/introduction) instead. + +## Before you start + +The tutorial assumes that the first factor is email password or social login, but the same set of steps are applicable for other first factor types. + + + +## Steps + + + +### 1. Configure the backend + + + +To start with, we configure the backend in the following way: + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import webauthn from "supertokens-node/recipe/webauthn"; +import Session from "supertokens-node/recipe/session"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + webauthn.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getMFARequirementsForAuth: async function (input) { + // Change this implementation if you want to require webauthn only for specific users + return [MultiFactorAuth.FactorIds.WEBAUTHN]; + }, + }; + }, + }, + }), + ], +}); +``` + + + + + +```python +from typing import Any, Awaitable, Callable, Dict, List, Optional, Union + +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + session, + thirdparty, + webauthn, +) +from supertokens_python.recipe.accountlinking.types import ( + AccountInfoWithRecipeIdAndUserId, + ShouldAutomaticallyLink, + ShouldNotAutomaticallyLink, +) +from supertokens_python.recipe.multifactorauth.types import ( + FactorIds, + OverrideConfig, + MFARequirementList, +) +from supertokens_python.recipe.multifactorauth.interfaces import RecipeInterface +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.types import User + + +async def should_link_webauthn_mfa_account( + new_account_info: AccountInfoWithRecipeIdAndUserId, + user: Optional[User], + current_session: Optional[SessionContainer], + tenant_id: str, + user_context: Dict[str, Any], +) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: + if current_session is None or current_session.get_tenant_id() != tenant_id: + return ShouldNotAutomaticallyLink() + + is_making_session_user_primary = ( + user is None + and new_account_info.recipe_user_id is not None + and new_account_info.recipe_user_id.get_as_string() + == current_session.get_recipe_user_id().get_as_string() + ) + is_linking_webauthn_to_session_user = ( + new_account_info.recipe_id == "webauthn" + and user is not None + and user.id == current_session.get_user_id() + ) + + if ( + not is_making_session_user_primary + and not is_linking_webauthn_to_session_user + ): + return ShouldNotAutomaticallyLink() + + return ShouldAutomaticallyLink(should_require_verification=True) + + +def override_functions(original_implementation: RecipeInterface): + async def get_mfa_requirements_for_auth( + tenant_id: str, + access_token_payload: Dict[str, Any], + completed_factors: Dict[str, int], + user: Callable[[], Awaitable[User]], + factors_set_up_for_user: Callable[[], Awaitable[List[str]]], + required_secondary_factors_for_user: Callable[[], Awaitable[List[str]]], + required_secondary_factors_for_tenant: Callable[[], Awaitable[List[str]]], + user_context: Dict[str, Any], + ) -> MFARequirementList: + # Change this implementation if you want to require webauthn only for specific users + return [FactorIds.WEBAUTHN] + + original_implementation.get_mfa_requirements_for_auth = ( + get_mfa_requirements_for_auth + ) + return original_implementation + + +init( + app_info=InputAppInfo( + app_name="Example App", + api_domain="http://localhost:3001", + website_domain="http://localhost:3000", + ), + supertokens_config=SupertokensConfig( + connection_uri="http://localhost:3567", + ), + framework="fastapi", + recipe_list=[ + session.init(), + thirdparty.init(), + emailpassword.init(), + accountlinking.init( + should_do_automatic_account_linking=should_link_webauthn_mfa_account + ), + webauthn.init(), + multifactorauth.init( + first_factors=[FactorIds.EMAILPASSWORD, FactorIds.THIRDPARTY], + override=OverrideConfig(functions=override_functions), + ), + ], +) +``` + + + +The MFA recipe override is required to indicate that `webauthn` must be completed before the user can access the app. + +Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "webauthn": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + + + + + + +In a multi tenancy setup, you may want to enable WebAuthn for all users, across all tenants, or for all users within specific tenants. For enabling for all users across all tenants, it's the same steps as in the [single tenant setup](#1-configure-the-backend) section above, so in this section, we will focus on enabling WebAuthn for all users within specific tenants. + +To start, we will initialise the WebAuthn and the MultiFactorAuth recipes in the following way: + + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + +```ts +import supertokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; +import webauthn from "supertokens-node/recipe/webauthn"; +import Session from "supertokens-node/recipe/session"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init(), + ThirdParty.init({ + //... + }), + EmailPassword.init({ + //... + }), + webauthn.init(), + MultiFactorAuth.init(), + ], +}); +``` + + + + + +```python +from typing import Any, Dict, Optional, Union + +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import ( + accountlinking, + emailpassword, + multifactorauth, + session, + thirdparty, + webauthn, +) +from supertokens_python.recipe.accountlinking.types import ( + AccountInfoWithRecipeIdAndUserId, + ShouldAutomaticallyLink, + ShouldNotAutomaticallyLink, +) +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.types import User + + +async def should_link_webauthn_mfa_account( + new_account_info: AccountInfoWithRecipeIdAndUserId, + user: Optional[User], + current_session: Optional[SessionContainer], + tenant_id: str, + user_context: Dict[str, Any], +) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: + if current_session is None or current_session.get_tenant_id() != tenant_id: + return ShouldNotAutomaticallyLink() + + is_making_session_user_primary = ( + user is None + and new_account_info.recipe_user_id is not None + and new_account_info.recipe_user_id.get_as_string() + == current_session.get_recipe_user_id().get_as_string() + ) + is_linking_webauthn_to_session_user = ( + new_account_info.recipe_id == "webauthn" + and user is not None + and user.id == current_session.get_user_id() + ) + + if ( + not is_making_session_user_primary + and not is_linking_webauthn_to_session_user + ): + return ShouldNotAutomaticallyLink() + + return ShouldAutomaticallyLink(should_require_verification=True) + + +init( + app_info=InputAppInfo( + app_name="Example App", + api_domain="http://localhost:3001", + website_domain="http://localhost:3000", + ), + supertokens_config=SupertokensConfig( + connection_uri="http://localhost:3567", + ), + framework="fastapi", + recipe_list=[ + session.init(), + thirdparty.init(), + emailpassword.init(), + accountlinking.init( + should_do_automatic_account_linking=should_link_webauthn_mfa_account + ), + webauthn.init(), + multifactorauth.init(), + ], +) +``` + + + +Unlike the single tenant setup, we do not provide any config to the `MultiFactorAuth` recipe cause all the necessary configuration will be done on a tenant level. + + + +To configure WebAuthn requirement for a tenant, we can call the following API: + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; +import MultiFactorAuth from "supertokens-node/recipe/multifactorauth"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + requiredSecondaryFactors: [MultiFactorAuth.FactorIds.WEBAUTHN], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + + + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate +from supertokens_python.recipe.multifactorauth.types import FactorIds + + +async def create_new_tenant(): + resp = await create_or_update_tenant( + "customer1", TenantConfigCreateOrUpdate( + first_factors=[FactorIds.EMAILPASSWORD], + required_secondary_factors=[FactorIds.WEBAUTHN], + ) + ) + + if resp.created_new: + # Tenant created successfully + pass + else: + # Existing tenant's config was modified + pass +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate +from supertokens_python.recipe.multifactorauth.types import FactorIds + + +def create_new_tenant(): + resp = create_or_update_tenant( + "customer1", TenantConfigCreateOrUpdate( + first_factors=[FactorIds.EMAILPASSWORD], + required_secondary_factors=[FactorIds.WEBAUTHN], + ) + ) + + if resp.created_new: + # Tenant created successfully + pass + else: + # Existing tenant's config was modified + pass +``` + + + + + + + +- In the above, we set the `firstFactors` to `["emailpassword", "thirdparty"]` to indicate that the first factor can be either `emailpassword` or `thirdparty`. +- We set the `requiredSecondaryFactors` to `["webauthn"]` to indicate that WebAuthn is required for all users in this tenant. The default implementation of `getMFARequirementsForAuth` in the `MultiFactorAuth` takes this into account. + + + +Once the user finishes the first factor (for example, with `emailpassword`), their session access token payload will look like this: +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939 + }, + "v": false + } +} +``` + +The `v` being `false` indicates that there are still factors that are pending. After the user has finished `webauthn`, the payload will look like: + +```json +{ + "st-mfa": { + "c": { + "emailpassword": 1702877939, + "webauthn": 1702877999 + }, + "v": true + } +} +``` + +Indicating that the user has finished all required factors, and should be allowed to access the app. + + + +### 2. Authorize account linking on the backend + +`shouldTryLinkingWithSessionUser: true` in the client calls below only asks the backend to try linking. It is not an +authorization decision. The backend `AccountLinking` policy must decide whether linking is allowed. + +SuperTokens automatically initializes `AccountLinking` with a deny-by-default policy when you omit the recipe. To use +WebAuthn as a second factor, initialize one explicitly configured `AccountLinking` recipe in the same recipe list. This +replaces the automatic default; do not add a second initialization. The Python examples above already include this +policy. For Node.js, add the configuration below to the recipe list shown above. If you already configure account +linking, merge these checks into that policy. + +The following policy only permits linking for the current session and tenant. It also requires verified account +information. SuperTokens and the Core still perform the authoritative conflict checks and reject linking if the recipe +user or account information belongs to another primary user. + +The single-tenant and multi-tenant Python examples above already initialize the configured `accountlinking` recipe +exactly once. Do not initialize it again. + + + +```ts +import { RecipeUserId, User } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +const accountLinkingForWebAuthnMFA = AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + ) => { + if (session === undefined || session.getTenantId() !== tenantId) { + return { shouldAutomaticallyLink: false }; + } + + const sessionRecipeUserId = session.getRecipeUserId().getAsString(); + const isMakingSessionUserPrimary = + user === undefined && newAccountInfo.recipeUserId?.getAsString() === sessionRecipeUserId; + const isLinkingWebAuthnToSessionUser = newAccountInfo.recipeId === "webauthn" && user?.id === session.getUserId(); + + if (!isMakingSessionUserPrimary && !isLinkingWebAuthnToSessionUser) { + return { shouldAutomaticallyLink: false }; + } + + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, +}); + +// Add accountLinkingForWebAuthnMFA once to the recipeList passed to supertokens.init. +``` + + + + + + +### 3. Configure the WebAuthn RP ID and origin + +WebAuthn validates the browser origin independently of your API domain. If your website is +`https://app.example.com` and your API is `https://api.example.com`, use the website origin for `origin`. The RP ID +must be the website hostname (`app.example.com`) or a registrable parent domain (`example.com`) whose scope you +intentionally accept. Production origins must use HTTPS. + +For tenant custom domains, keep the allowed RP ID and exact origin in server-side configuration or a trusted database +indexed by the validated tenant ID. Reject unknown tenants. Never derive or reflect either value from `Origin`, +`Host`, `X-Forwarded-Host`, or other request headers: an attacker may control those headers, and changing RP values can +break credential scoping or allow ceremonies for an unintended domain. + + + +```ts +import WebAuthn from "supertokens-node/recipe/webauthn"; + +const relyingPartyByTenant: Record = { + public: { + relyingPartyId: "example.com", + origin: "https://app.example.com", + }, + customer1: { + relyingPartyId: "login.customer.example", + origin: "https://login.customer.example", + }, +}; + +function getRelyingParty(tenantId: string) { + const relyingParty = relyingPartyByTenant[tenantId]; + if (relyingParty === undefined) { + throw new Error("WebAuthn is not configured for this tenant"); + } + return relyingParty; +} + +const webAuthnWithTrustedRelyingParties = WebAuthn.init({ + getRelyingPartyId: async ({ tenantId }) => getRelyingParty(tenantId).relyingPartyId, + getOrigin: async ({ tenantId }) => getRelyingParty(tenantId).origin, +}); + +// Use webAuthnWithTrustedRelyingParties instead of webauthn.init() in the recipeList above. +``` + + + + + +```python +from typing import Dict, Optional + +from supertokens_python.framework import BaseRequest +from supertokens_python.recipe import webauthn +from supertokens_python.recipe.webauthn import WebauthnConfig +from supertokens_python.types.base import UserContext + +relying_party_by_tenant: Dict[str, Dict[str, str]] = { + "public": { + "relying_party_id": "example.com", + "origin": "https://app.example.com", + }, + "customer1": { + "relying_party_id": "login.customer.example", + "origin": "https://login.customer.example", + }, +} + + +def get_relying_party(tenant_id: str) -> Dict[str, str]: + relying_party = relying_party_by_tenant.get(tenant_id) + if relying_party is None: + raise ValueError("WebAuthn is not configured for this tenant") + return relying_party + + +async def get_relying_party_id( + *, + tenant_id: str, + request: Optional[BaseRequest], + user_context: UserContext, +) -> str: + return get_relying_party(tenant_id)["relying_party_id"] + + +async def get_origin( + *, + tenant_id: str, + request: Optional[BaseRequest], + user_context: UserContext, +) -> str: + return get_relying_party(tenant_id)["origin"] + + +web_authn_with_trusted_relying_parties = webauthn.init( + config=WebauthnConfig( + get_relying_party_id=get_relying_party_id, + get_origin=get_origin, + ) +) + +# Use web_authn_with_trusted_relying_parties instead of webauthn.init() in the recipe_list above. +``` + + + +### 4. Configure the frontend + + + + + +We start by modifying the `init` function call on the frontend like so: + + + + + +You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: + +This change is in your auth route config. + + + + + +```tsx +import supertokens from "supertokens-auth-react"; +import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import webauthn from "supertokens-auth-react/recipe/webauthn"; + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + webauthn.init(), + MultiFactorAuth.init(), + Multitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); +``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) + +supertokensUIInit({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + supertokensUIWebAuthn.init(), + supertokensUIMultiFactorAuth.init(), + supertokensUIMultitenancy.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + getTenantId: async (context) => { + return "TODO"; + }, + }; + }, + }, + }), + ], +}); +``` + + + + + +This change goes in the `supertokens-web-js` SDK config at the root of your application: + + + + + +```tsx +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import WebAuthn from "supertokens-web-js/recipe/webauthn"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + WebAuthn.init(), + ], +}); +``` + + + + + + + + + +You will have to make changes to the auth route config, as well as to the `supertokens-web-js` SDK config at the root of your application: + +This change is in your auth route config. + + + + + +```tsx +import supertokens from "supertokens-auth-react"; +import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth"; +import webauthn from "supertokens-auth-react/recipe/webauthn"; + +supertokens.init({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + webauthn.init(), + MultiFactorAuth.init({ + firstFactors: [MultiFactorAuth.FactorIds.EMAILPASSWORD, MultiFactorAuth.FactorIds.THIRDPARTY], + }), + ], +}); +``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) +supertokensUIInit({ + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + // other recipes.. + supertokensUIWebAuthn.init(), + supertokensUIMultiFactorAuth.init({ + firstFactors: [ + supertokensUIMultiFactorAuth.FactorIds.EMAILPASSWORD, + supertokensUIMultiFactorAuth.FactorIds.THIRDPARTY, + ], + }), + ], +}); +``` + + + + + +This change goes in the `supertokens-web-js` SDK config at the root of your application: + + + + + +```tsx +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import WebAuthn from "supertokens-web-js/recipe/webauthn"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + WebAuthn.init(), + ], +}); +``` + + + + + + +On the frontend, the `MultiFactorAuth` recipe initialization only requires the first factors to be configured. +The secondary factors will be determined based on a request to the backend. + +Add the WebAuthn pre-built UI to render the SuperTokens component: + + + +:::success[This step is not required for non React apps, since all the pre-built UI components are already added into the bundle.] +::: + + + + + + + +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; +import reactRouterDOM, { Routes, BrowserRouter as Router, Route } from "react-router-dom"; + +function App() { + return ( + +
+ +
+ + {getSuperTokensRoutesForReactRouterDom(reactRouterDOM, [ + /* ... */ WebauthnPreBuiltUI, + MultiFactorAuthPreBuiltUI, + ])} + // ... other routes + +
+
+
+
+ ); +} +``` +
+ +```tsx +import { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; +import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; +import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui"; + +function App() { + if (canHandleRoute([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI])) { + return getRoutingComponent([/* ... */ WebauthnPreBuiltUI, MultiFactorAuthPreBuiltUI]); + } + return {/*Your app*/}; +} +``` + +
+
+ + + +
+ +
+ + + + +We start by initialising the MFA and WebAuthn recipe on the frontend like so: + + + + + +:::success[This step is not applicable for mobile apps. Please continue reading.] +::: + + + + + + + +```tsx +import SuperTokens from "supertokens-web-js"; +import MultiFactorAuth from "supertokens-web-js/recipe/multifactorauth"; +import WebAuthn from "supertokens-web-js/recipe/webauthn"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + MultiFactorAuth.init(), + WebAuthn.init(), + ], +}); +``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +supertokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [ + // other recipes... + supertokensMultiFactorAuth.init(), + supertokensWebAuthn.init(), + ], +}); +``` + + + + + + + + + + +After the first factor login, you should start by checking the access token payload and see if the MFA claim's `v` boolean is `false`. +'If it's not, then you can redirect the user to the application page. + +If it's `false`, the frontend then needs to [call the MFA endpoint](/references/fdi/multifactorauth-recipe/getmfainfo) to get information about which factor the user should be asked to complete next. +Based on the initial backend configuration, the `next` array will contain `["webauthn"]`. + +To complete the secondary factor you need to take into account if the users has previously configured a passkey or not. +You can determine this by checking if the `alreadySetup` array contains `"webauthn"`. + + +#### Sign up flow + + + + + +Support for this flow is not available in the mobile SDK. +You will have to call the [backend API](/references/fdi/introduction) directly. + +First, call the [**Register WebAuthn Credential**](/references/fdi/webauthn-recipe/webauthnregistercredential) endpoint to register the passkey. +Afterwards call the [**Sign Up with WebAuthn**](/references/fdi/webauthn-recipe/webauthnsignup) to complete the second factor sign up process. + + + + + + + +```ts +import Webauthn from "supertokens-web-js/recipe/webauthn"; + +async function secondFactorSignUp(email: string, userContext: Record) { + const response = await Webauthn.registerCredentialWithSignUp({ + email, + shouldTryLinkingWithSessionUser: true, + userContext, + }); + + return response.status === "OK"; +} +``` + + +```ts check=false reason="script-tag example relies on the WebAuthn global provided by the loaded SuperTokens bundle" +async function secondFactorSignUp(email: string, userContext: Record) { + const response = await supertokensWebAuthn.registerCredentialWithSignUp({ + email, + shouldTryLinkingWithSessionUser: true, + userContext, + }); + + return response.status === "OK"; +} +``` + + + + + + + + + + +#### Sign in flow + + + + + +Support for this flow is not available in the mobile SDK. +You will have to call the [backend API](/references/fdi/introduction) directly. + +Call the [**Sign in with WebAuthn**](/references/fdi/webauthn-recipe/webauthnsignin) endpoint to complete the secondary factor flow. + + + + + + + +```ts +import Webauthn from "supertokens-web-js/recipe/webauthn"; + +async function secondFactorSignUp(userContext: Record) { + const response = await Webauthn.authenticateCredentialWithSignIn({ + shouldTryLinkingWithSessionUser: true, + userContext, + }); + + return response.status === "OK"; +} +``` + + +```ts check=false reason="script-tag example relies on the WebAuthn global provided by the loaded SuperTokens bundle" +async function secondFactorSignUp(userContext: Record) { + const response = await supertokensWebAuthn.authenticateCredentialWithSignIn({ + shouldTryLinkingWithSessionUser: true, + userContext, + }); + + return response.status === "OK"; +} +``` + + + + + + + + + + + + + + +That's it! :tada: + +Based on this configuration, users first access the authentication form which shows the `emailpassword` and `thirdparty` options. +After first factor completion, they access the WebAuthn form to finalize the authentication attempt. diff --git a/docs/additional-verification/session-verification/_blocks/session-object.mdx b/docs/additional-verification/session-verification/_blocks/session-object.mdx deleted file mode 100644 index e40fb02bbd..0000000000 --- a/docs/additional-verification/session-verification/_blocks/session-object.mdx +++ /dev/null @@ -1,458 +0,0 @@ - - - - -```tsx -import { RecipeUserId } from "supertokens-node"; // typecheck-only -import { ReqResInfo } from "supertokens-node/recipe/session/types"; // typecheck-only -import { JSONObject } from "supertokens-node/types"; // typecheck-only -import { SessionClaimValidator, SessionClaim } from "supertokens-website"; // typecheck-only - -interface Session { - /** - * Destroys this session in the database and on the frontend. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the session is successfully revoked. - */ - revokeSession(userContext?: Record): Promise; - - /** - * Retrieves the session data stored in the database associated with the session. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves with the session data. - */ - getSessionDataFromDatabase(userContext?: Record): Promise; - - /** - * Sets a new JSON object to the session data stored in the database. - * @param newSessionData The new session data to store. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the session data is updated. - */ - updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; - - /** - * Returns the user ID of the logged-in user. - * @param userContext Optional context object for additional data. - * @returns The user ID as a string. - */ - getUserId(userContext?: Record): string; - - /** - * Returns the `RecipeUserId` object for the session. It represents the user ID of the specific login method for this user. - * @param userContext Optional context object for additional data. - * @returns The `RecipeUserId`. - */ - getRecipeUserId(userContext?: Record): RecipeUserId; - - /** - * Returns the tenant ID of the session. The default value is "public" if multi-tenancy is not used. - * @param userContext Optional context object for additional data. - * @returns The tenant ID as a string. - */ - getTenantId(userContext?: Record): string; - - /** - * Returns the access token's payload for this session. This includes user-defined claims, standard claims, and SuperTokens specific ones. - * @param userContext Optional context object for additional data. - * @returns The access token payload. - */ - getAccessTokenPayload(userContext?: Record): any; - - /** - * Returns the `sessionHandle` for this session, a unique string constant for each session. - * @param userContext Optional context object for additional data. - * @returns The session handle as a string. - */ - getHandle(userContext?: Record): string; - - /** - * Returns an object containing the raw string representation of all tokens associated with the session, along with an update status. - * @returns An object with accessToken, refreshToken, antiCsrfToken, frontToken, and accessAndFrontTokenUpdated. - */ - getAllSessionTokensDangerously(): { - accessToken: string; - refreshToken: string | undefined; - antiCsrfToken: string | undefined; - frontToken: string; - accessAndFrontTokenUpdated: boolean; - }; - - /** - * Returns the raw string access token for this session. - * @param userContext Optional context object for additional data. - * @returns The access token as a string. - */ - getAccessToken(userContext?: Record): string; - - /** - * Adds key/value pairs into a JSON object in the access token. Setting a key to null removes it from the payload. - * @param accessTokenPayloadUpdate The updates to apply to the access token payload. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the payload is updated. - */ - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; - - /** - * Returns the time in milliseconds of when this session was created. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves with the creation time in milliseconds. - */ - getTimeCreated(userContext?: Record): Promise; - - /** - * Returns the time in milliseconds of when this session will expire if not refreshed. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves with the expiry time in milliseconds. - */ - getExpiry(userContext?: Record): Promise; - - /** - * Asserts the validity of custom session claims using provided validators. - * @param claimValidators An array of session claim validators. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the claim assertions are complete. - */ - assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; - - /** - * Fetches and sets a custom claim in the session. - * @param claim The session claim to fetch and set. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves with the fetched claim. - */ - fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; - - /** - * Sets the value of a session claim. - * @param claim The session claim to update. - * @param value The new value for the claim. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the claim value is set. - */ - setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; - - /** - * Gets the value of a session claim. - * @param claim The session claim to retrieve the value for. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves with the claim value, or undefined if not found. - */ - getClaimValue(claim: SessionClaim, userContext?: Record): Promise; - - /** - * Removes a session claim. - * @param claim The session claim to remove. - * @param userContext Optional context object for additional data. - * @returns A promise that resolves when the claim is removed. - */ - removeClaim(claim: SessionClaim, userContext?: Record): Promise; - - /** - * Attaches the session to a request-response cycle. - * @param reqResInfo Information about the request-response. - * @param userContext Optional context object for additional data. - * @returns A promise or void once the session is attached. - */ - attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record): Promise | void; -} -``` - - - - -### `getSessionDataFromDatabase` vs `getAccessTokenPayload` - - - - -| | `getSessionDataFromDatabase` | `getAccessTokenPayload` | -|--------------------------------------|-----------------------------|-------------------------| -| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | -| **Speed** | Slower (requires a network call) | Faster (no network call required) | -| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | -| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | -| **Persistence** | Updated via `updateSessionDataInDatabase` | Updated via `mergeIntoAccessTokenPayload` | - - - - -```go - -import ( // typecheck-only - "github.com/supertokens/supertokens-golang/recipe/session/claims" // typecheck-only - "github.com/supertokens/supertokens-golang/supertokens" // typecheck-only -) // typecheck-only - -type TypeSessionContainer struct { - // Destroys this session in the database and on the frontend. - RevokeSession func() error - - // Retrieves the session data stored in the database associated with the session. - GetSessionDataInDatabase func() (map[string]interface{}, error) - - // Sets a new JSON object to the session data stored in the database. - // `newSessionData` is the new session data to store. - UpdateSessionDataInDatabase func(newSessionData map[string]interface{}) error - - // Returns the user ID of the logged-in user. - GetUserID func() string - - // Returns the tenant ID of the session. - // Default value is "public" if multi-tenancy is not used. - GetTenantId func() string - - // Returns the access token's payload for this session. - // Includes user-defined claims, standard claims, and SuperTokens specific ones. - GetAccessTokenPayload func() map[string]interface{} - - // Returns the `sessionHandle` for this session, - // a unique string constant for each session. - GetHandle func() string - - // Returns an object containing the raw string representation - // of all tokens associated with the session, along with an update status. - GetAllSessionTokensDangerously func() SessionTokens - - // Returns the raw string access token for this session. - GetAccessToken func() string - - // Returns the time in milliseconds of when this session was created. - GetTimeCreated func() (uint64, error) - - // Returns the time in milliseconds of when this session will expire if not refreshed. - GetExpiry func() (uint64, error) - - // Context-aware methods that provide the same functionality as their counterparts above while considering user context - - // Destroys this session in the database and on the frontend with user context. - RevokeSessionWithContext func(userContext supertokens.UserContext) error - - // Retrieves the session data stored in the database associated with the session with user context. - GetSessionDataInDatabaseWithContext func(userContext supertokens.UserContext) (map[string]interface{}, error) - - // Sets a new JSON object to the session data stored in the database with user context. - UpdateSessionDataInDatabaseWithContext func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error - - // Returns the user ID of the logged-in user with user context. - GetUserIDWithContext func(userContext supertokens.UserContext) string - - // Returns the tenant ID of the session with user context. - GetTenantIdWithContext func(userContext supertokens.UserContext) string - - // Returns the access token's payload for this session with user context. - GetAccessTokenPayloadWithContext func(userContext supertokens.UserContext) map[string]interface{} - - // Returns the `sessionHandle` for this session with user context. - GetHandleWithContext func(userContext supertokens.UserContext) string - - // Returns the raw string access token for this session with user context. - GetAccessTokenWithContext func(userContext supertokens.UserContext) string - - // Returns the time in milliseconds of when this session was created with user context. - GetTimeCreatedWithContext func(userContext supertokens.UserContext) (uint64, error) - - // Returns the time in milliseconds of when this session will expire if not refreshed with user context. - GetExpiryWithContext func(userContext supertokens.UserContext) (uint64, error) - - // Adds key/value pairs into a JSON object in the access token with user context. - // Setting a key to nil removes it from the payload. - MergeIntoAccessTokenPayloadWithContext func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error - - // Asserts the validity of custom session claims using provided validators with user context. - AssertClaimsWithContext func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error - - // Fetches and sets a custom claim in the session with user context. - FetchAndSetClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error - - // Sets the value of a session claim with user context. - SetClaimValueWithContext func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error - - // Gets the value of a session claim with user context. - // Returns the value or nil if not found. - GetClaimValueWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} - - // Removes a session claim with user context. - RemoveClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error - - // Attaches the session to a request-response cycle with user context. - AttachToRequestResponseWithContext func(info RequestResponseInfo, userContext supertokens.UserContext) error - - // Adds key/value pairs into a JSON object in the access token. - // Setting a key to nil removes it from the payload. - MergeIntoAccessTokenPayload func(accessTokenPayloadUpdate map[string]interface{}) error - - // Asserts the validity of custom session claims using provided validators. - AssertClaims func(claimValidators []claims.SessionClaimValidator) error - - // Fetches and sets a custom claim in the session. - FetchAndSetClaim func(claim *claims.TypeSessionClaim) error - - // Sets the value of a session claim. - SetClaimValue func(claim *claims.TypeSessionClaim, value interface{}) error - - // Gets the value of a session claim. - // Returns the value or nil if not found. - GetClaimValue func(claim *claims.TypeSessionClaim) interface{} - - // Removes a session claim. - RemoveClaim func(claim *claims.TypeSessionClaim) error - - // Attaches the session to a request-response cycle. - AttachToRequestResponse func(info RequestResponseInfo) error -} -``` - - - -### `GetSessionDataFromDatabase` vs `GetAccessTokenPayload` - - - -| | `GetSessionDataFromDatabase` | `GetAccessTokenPayload` | -|--------------------------------------|-----------------------------|-------------------------| -| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | -| **Speed** | Slower (requires a network call) | Faster (no network call required) | -| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | -| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | -| **Persistence** | Updated via `UpdateSessionDataInDatabase` | Updated via `MergeIntoAccessTokenPayload` | - - - - - -```python -# exclude-from-type-checking - -class Session: - # Destroys this session in the database and on the frontend. - # Optional user_context can be used for additional contextual data. - async def revoke_session(self, user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Retrieves the session data stored in the database associated with the session. - # Optional user_context can be used for additional contextual data. - async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - pass - - # Sets a new JSON object to the session data stored in the database. - # `new_session_data` is the new session data to store. - # Optional user_context can be used for additional contextual data. - async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Returns the user ID of the logged-in user. - # Optional user_context can be used for additional contextual data. - def get_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: - pass - - # Returns the `RecipeUserId` object for the session. - # This represents the user ID of the specific login method for this user. - # Optional user_context can be used for additional contextual data. - def get_recipe_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> RecipeUserId: - pass - - # Returns the tenant ID of the session. - # Default value is "public" if multi-tenancy is not used. - # Optional user_context can be used for additional contextual data. - def get_tenant_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: - pass - - # Returns the access token's payload for this session. - # Includes user-defined claims, standard claims, and SuperTokens specific ones. - # Optional user_context can be used for additional contextual data. - def get_access_token_payload(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - pass - - # Returns the `sessionHandle` for this session, - # a unique string constant for each session. - # Optional user_context can be used for additional contextual data. - def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: - pass - - # Returns an object containing the raw string representation - # of all tokens associated with the session, along with an update status. - def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: - pass - - # Returns the raw string access token for this session. - # Optional user_context can be used for additional contextual data. - def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: - pass - - # Adds key/value pairs into a JSON object in the access token. - # Setting a key to None removes it from the payload. - # `access_token_payload_update` contains the updates to apply. - # Optional user_context can be used for additional contextual data. - async def merge_into_access_token_payload(self, access_token_payload_update: JSONObject, user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Returns the time in milliseconds of when this session was created. - # Optional user_context can be used for additional contextual data. - async def get_time_created(self, user_context: Optional[Dict[str, Any]] = None) -> int: - pass - - # Returns the time in milliseconds of when this session will expire if not refreshed. - # Optional user_context can be used for additional contextual data. - async def get_expiry(self, user_context: Optional[Dict[str, Any]] = None) -> int: - pass - - # Asserts the validity of custom session claims using provided validators. - # `claim_validators` is an array of session claim validators. - # Optional user_context can be used for additional contextual data. - async def assert_claims(self, claim_validators: List[SessionClaimValidator], user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Fetches and sets a custom claim in the session. - # `claim` is the session claim to fetch and set. - # Optional user_context can be used for additional contextual data. - async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Sets the value of a session claim. - # `claim` is the session claim to update. - # `value` is the new value for the claim. - # Optional user_context can be used for additional contextual data. - async def set_claim_value(self, claim: SessionClaim[_T], value: _T, user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Gets the value of a session claim. - # `claim` is the session claim to retrieve the value for. - # Optional user_context can be used for additional contextual data. - # Returns a promise that resolves with the claim value, or None if not found. - async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) -> Union[_T, None]: - pass - - # Removes a session claim. - # `claim` is the session claim to remove. - # Optional user_context can be used for additional contextual data. - async def remove_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: - pass - - # Attaches the session to a request-response cycle. - # `req_res_info` contains information about the request-response. - # Optional user_context can be used for additional contextual data. - async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod, user_context: Optional[Dict[str, Any]] = None) -> None: - pass -``` - - - - -### `get_session_data_from_database` vs `get_access_token_payload` - - - - -| | `get_session_data_from_database` | `get_access_token_payload` | -|--------------------------------------|-----------------------------|-------------------------| -| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | -| **Speed** | Slower (requires a network call) | Faster (no network call required) | -| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | -| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | -| **Persistence** | Updated via `update_session_data_in_database` | Updated via `merge_into_access_token_payload` | - - - - - diff --git a/docs/additional-verification/session-verification/_category_.json b/docs/additional-verification/session-verification/_category_.json deleted file mode 100644 index 13cc1d829d..0000000000 --- a/docs/additional-verification/session-verification/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Session Verification", - "position": 1 -} diff --git a/docs/additional-verification/session-verification/claim-validation.mdx b/docs/additional-verification/session-verification/claim-validation.mdx index dae4b24bb0..8491062fa6 100644 --- a/docs/additional-verification/session-verification/claim-validation.mdx +++ b/docs/additional-verification/session-verification/claim-validation.mdx @@ -1,26 +1,24 @@ --- title: Claims validation -hide_title: true -toc_max_heading_level: 5 -sidebar_position: 5 description: Validate your resources based on claims that get attached to the session. -page_type: guide -recipe: session -category: session-verification +sidebar: + order: 5 --- - -# Claim validation - ## Overview -**SuperTokens** provides two approaches for managing access control: +**SuperTokens** provides two approaches for working with authorization data: 1. **Session Claims**: An abstraction that includes automatic validation and refresh capabilities 2. **Access Token Payload**: A basic way to check the token payload -In most of the cases the recommended way is to use session claims. -You can use next table to understand the differences between the two approaches. +In most cases, the recommended approach is to use session claims. +Use the following table to understand the differences between the two approaches. + +:::caution[Enforce authorization on the backend] +Frontend claim checks are user-experience controls only. Client code and client-readable payloads can be bypassed or +modified. Every protected API must verify the session and enforce its required claim validators on the backend. +::: | Feature | Session Claims | Access Token Payload | | ------------------------------------ | -------------- | -------------------- | @@ -36,50 +34,45 @@ This guide shows you how to use each method. ## References - - -## Session Claim - + + ### Session claim interface ```tsx import { RecipeUserId } from "supertokens-node"; +import { JSONObject, UserContext } from "supertokens-node/types"; -type JSONObject = any; // REMOVE_FROM_OUTPUT interface SessionClaim { - // Unique identifier for the claim. - // For a `boolean` claim (for example if the email is verified or not), this would be a string like `"st-ev"`. - readonly key: string; - - /** - * Fetches the current value of this claim for the user. - * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database - * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. - */ - fetchValue( - userId: string, - recipeUserId: RecipeUserId, - tenantId: string, - currentPayload: JSONObject | undefined, - // @ts-expect-error - userContext: UserContext - ): Promise | T | undefined; - - /** - * Removes the claim from the payload, by cloning and updating the entire object. - * - * @returns The modified payload object - */ - // @ts-expect-error - removeFromPayload(payload: JSONObject, userContext: UserContext): JSONObject; - - /** - * Gets the value of the claim stored in the payload - * - * @returns Claim value - */ - // @ts-expect-error - getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; + // Unique identifier for the claim. + // For a `boolean` claim (for example if the email is verified or not), this would be a string like `"st-ev"`. + readonly key: string; + + /** + * Fetches the current value of this claim for the user. + * The undefined return value signifies that we don't want to update the claim payload and or the claim value is not present in the database + * This can happen for example with a second factor auth claim, where we don't want to add the claim to the session automatically. + */ + fetchValue( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + currentPayload: JSONObject | undefined, + userContext: UserContext, + ): Promise | T | undefined; + + /** + * Removes the claim from the payload, by cloning and updating the entire object. + * + * @returns The modified payload object + */ + removeFromPayload(payload: JSONObject, userContext: UserContext): JSONObject; + + /** + * Gets the value of the claim stored in the payload + * + * @returns Claim value + */ + getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; } ``` @@ -94,21 +87,20 @@ All the recipe claims are built around these primitives: - [`PermissionClaim`](https://github.com/supertokens/supertokens-node/blob/master/lib/ts/recipe/userroles/permissionClaim.ts): This stores the list of permissions associated with the user. - #### On the frontend Like the backend, the frontend also has the concept of session claim objects which need to conform to the following interface: ```tsx type SessionClaim = { - // Refresh the claim values based on an async API call - refresh(): Promise; - - // Returns the value from the session claim - getValueFromPayload(payload: any): T | undefined; - - // Returns the last time the claim was refreshed - getLastFetchedTime(payload: any): number | undefined; + // Refresh the claim values based on an async API call + refresh(userContext: any): Promise; + + // Returns the value from the session claim + getValueFromPayload(payload: any, userContext: any): T | undefined; + + // Returns the last time the claim was refreshed + getLastFetchedTime(payload: any, userContext: any): number | undefined; }; ``` @@ -119,70 +111,68 @@ Like the backend SDK, the frontend SDK also exposes a few base claims: - [`BooleanClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/booleanClaim.ts) - [`PrimitiveClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveClaim.ts) - [`PrimitiveArrayClaim`](https://github.com/supertokens/supertokens-website/blob/master/lib/ts/claims/primitiveArrayClaim.ts) - -## Claim Validator - + + Once you add a claim to the session, specify the checks that need to run on them during session verification. For example, if an API should allow access only to `admin` roles, there must be a way to tell SuperTokens to do that check. This is where claim validators come into the picture. Here is the shape for a claim validator object: -```tsx -type SessionClaim = any; // REMOVE_FROM_OUTPUT +```tsx check=false reason="interface excerpt depends on SessionClaim defined in the preceding reference" type SessionClaimValidator = { - // Identifies the session claim validator - // Used to know which validator failed in case multiple of them undergo checking at the same time. - // The value of this is typically the same as the claim object's `key`, but you can set it to anything else. - id: string; - - // A reference to the claim object that's associated with this validator. - claim: SessionClaim; - - // Determines if the value of the claim should undergo fetching again. - // In the built-in validators, this function typically returns `true` if the claim does not exist in the `payload`, or if it's too old. - shouldRefetch: (payload: any, userContext: any) => Promise; - - /** extracts the claim value from the input `payload` (typically using `claim.getValueFromPayload`), and determines if the validator check has passed or not. - * For example, if the validator aims to enforce that the user has verified their email, and if the claim value is `false`, then this function would return: - * { - * isValid: false, - * reason: { - * message: "wrong value", - * expectedValue: true, - * actualValue: false - * } - * } - */ - validate: (payload: any, userContext: any) => Promise; + // Identifies the session claim validator + // Used to know which validator failed in case multiple of them undergo checking at the same time. + // The value of this is typically the same as the claim object's `key`, but you can set it to anything else. + id: string; + + // A reference to the claim object that's associated with this validator. + claim: SessionClaim; + + // Determines if the value of the claim should undergo fetching again. + // In the built-in validators, this function typically returns `true` if the claim does not exist in the `payload`, or if it's too old. + shouldRefetch: (payload: any, userContext: any) => boolean | Promise; + + /** extracts the claim value from the input `payload` (typically using `claim.getValueFromPayload`), and determines if the validator check has passed or not. + * For example, if the validator aims to enforce that the user has verified their email, and if the claim value is `false`, then this function would return: + * { + * isValid: false, + * reason: { + * message: "wrong value", + * expectedValue: true, + * actualValue: false + * } + * } + */ + validate: (payload: any, userContext: any) => Promise; }; type ClaimValidationResult = { isValid: true } | { isValid: false; reason?: any }; ``` -Using this interface and the claims interface, SuperTokens runs the following session claim validation process during session verification: +Conceptually, SuperTokens runs the following session claim validation process during session verification. This +pseudocode omits recipe user ID, tenant ID, user context, asynchronous operations, and the actual payload update flow: -```tsx -// @ts-nocheck +```tsx check=false reason="algorithm pseudocode intentionally omits declarations and concrete SDK types" function validateSessionClaims(accessToken, claimValidators) { - payload = accessToken.getPayload(); - - // Step 1: refetch claims if required - for(validator in claimValidators) { - if (validator.shouldRefetch(payload)) { - claimValue = validator.claim.fetchValue(accessToken.sub) - payload = validator.claim.addToPayload_internal(payload, claimValue) - } + payload = accessToken.getPayload(); + + // Step 1: refetch claims if required + for (validator in claimValidators) { + if (validator.shouldRefetch(payload)) { + claimValue = validator.claim.fetchValue(accessToken.sub); + payload = validator.claim.addToPayload_internal(payload, claimValue); } + } - failedClaims = [] + failedClaims = []; - // Step 2: Validate all claims - for(validator in claimValidators) { - validationResult = validator.validate(payload) - if (!validationResult.isValid) { - failedClaims.push({id: validator.id, reason: validationResult.reason}) - } + // Step 2: Validate all claims + for (validator in claimValidators) { + validationResult = validator.validate(payload); + if (!validationResult.isValid) { + failedClaims.push({ id: validator.id, reason: validationResult.reason }); } + } - return failedClaims + return failedClaims; } ``` @@ -201,7 +191,9 @@ The built-in base claims (`PrimitiveClaim`, `PrimitiveArrayClaim`, `BooleanClaim In all the above claim validators, the `maxAgeInSeconds`/`maxAge` input (which is optional) governs how often to refetch the session claim value: - A value of `0` causes it to refetch the claim value each time a check happens. -- If not passed, the claim is only refetched if it's missing in the session. The built-in claims like email verification or user roles claims have a default value of five minutes, meaning that those claim values refresh from the database after every five minutes. +- If not passed, base claim validators refetch only when the claim is missing. Recipe-specific validators can use other + defaults. For example, the email verification `isVerified` validator refetches a `false` value after ten seconds by + default and has no default maximum age for a `true` value. The user roles validators do not set a default maximum age. ```tsx interface SessionClaim { @@ -211,13 +203,14 @@ interface SessionClaim { getValueFromPayload(payload: any): T | undefined; } ``` - - + ## Before you start - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: --- @@ -243,7 +236,7 @@ const SecondFactorClaim = new BooleanClaim({ #### Backend global validation -```tsx +```tsx check=false reason="example uses the application-defined SecondFactorClaim from the preceding setup" import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; @@ -263,7 +256,6 @@ SuperTokens.init({ return { ...originalImplementation, getGlobalClaimValidators: async function (input) { - // @ts-expect-error return [...input.claimValidatorsAddedByOtherRecipes, SecondFactorClaim.validators.isTrue()]; }, }; @@ -299,6 +291,8 @@ app.post( #### Frontend validation +This controls frontend rendering only. Apply the equivalent validator to every protected backend route, as shown above. + ```tsx import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; @@ -322,11 +316,10 @@ const AdminRoute = (props: React.PropsWithChildren) => { #### Backend custom error handling -```tsx +```tsx check=false reason="walkthrough excerpt relies on helper functions or values defined in surrounding steps" import { Error as STError } from "supertokens-node/recipe/session"; import { UserRoleClaim } from "supertokens-node/recipe/userroles"; -// @ts-expect-error if (roles === undefined || !roles.includes("admin")) { throw new STError({ type: "INVALID_CLAIMS", @@ -371,10 +364,10 @@ The access token payload is a simple way to store custom data that needs to be a ### 1. Add Custom Claims to the Access Token Payload -:::important -The access token payload has a set of default claims that can not be overwritten. -They reserve these for standard or SuperTokens specific use-cases. -Those claims are: `sub`, `iat`, `exp`, `sessionHandle`, `refreshTokenHash1`, `parentRefreshTokenHash1`, `antiCsrfToken` +:::note[The access token payload has a set of protected claims that cannot be overwritten.] +SuperTokens reserves these for standard or internal use cases. +Those claims are: `sub`, `iat`, `exp`, `sessionHandle`, `refreshTokenHash1`, `parentRefreshTokenHash1`, `antiCsrfToken`, +`rsub`, `tId`, and `stt`. Trying to overwrite these values results in errors in the authentication flow process. ::: @@ -459,6 +452,8 @@ app.get("/myApi", verifySession(), async (req, res) => { #### On the frontend +Use frontend payload values only for display and navigation. Do not authorize access to backend data from this check. + ```tsx import Session from "supertokens-auth-react/recipe/session"; @@ -468,17 +463,16 @@ async function someFunc() { let customClaimValue = accessTokenPayload.customClaim; } } - ``` --- ## See also - - - - - - - + + + + + + + diff --git a/docs/additional-verification/session-verification/meta.ts b/docs/additional-verification/session-verification/meta.ts new file mode 100644 index 0000000000..33857e8564 --- /dev/null +++ b/docs/additional-verification/session-verification/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Session Verification", + icon: "badge-check", + order: 1, +}); diff --git a/docs/additional-verification/session-verification/protect-api-routes.mdx b/docs/additional-verification/session-verification/protect-api-routes.mdx index aba9015a1c..be83db44ee 100644 --- a/docs/additional-verification/session-verification/protect-api-routes.mdx +++ b/docs/additional-verification/session-verification/protect-api-routes.mdx @@ -1,34 +1,22 @@ --- title: Protect backend routes -hide_title: true -sidebar_position: 1 -description: >- - Explore three session verification methods with SuperTokens: middleware, - function, and manual JWT verification. -type: guide -page_type: guide -recipe: session -category: session-verification +description: "Protect backend routes with SuperTokens session middleware and session verification APIs." +sidebar: + order: 1 --- - -import SessionObject from "./_blocks/session-object.mdx"; - - -# Protect API routes - ## Overview -You can choose between three different methods to check for a session inside an API route handler. -The easiest way to do it is to use the `Verify Session` middleware. -Also, depending on your use case, you can directly fetch the session or manually verify the JWT. -Check each method to see which one works for you. +Use the `Verify Session` middleware when your framework supports middleware. Otherwise, call `Get Session` directly. +Both methods validate the complete SuperTokens session token and configured session claims. Manual JWT verification is a +fallback for platforms without a released SuperTokens backend SDK. ## Before you start - - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: --- @@ -38,11 +26,20 @@ This function acts as a middleware inside your API endpoints. Hence, it requires that your backend framework supports the concept of middlewares. Besides checking for a session, it also writes responses to the client on its own, based on the session's validity and the provided configuration. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -50,17 +47,13 @@ import { SessionRequest } from "supertokens-node/framework/express"; let app = express(); -// highlight-start app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - // highlight-end - //.... + let userId = req.session!.getUserId(); + //.... }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -69,26 +62,23 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - //highlight-start - options: { - pre: [ - { - method: verifySession() - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - //highlight-end - //... - } -}) + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -96,35 +86,32 @@ import { SessionRequest } from "supertokens-node/framework/fastify"; let fastify = Fastify(); -//highlight-start -fastify.post("/like-comment", { +fastify.post( + "/like-comment", + { preHandler: verifySession(), -}, (req: SessionRequest, res) => { + }, + (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - //highlight-end //.... -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { - let userId = awsEvent.session!.getUserId(); - //.... -}; + let userId = awsEvent.session!.getUserId(); + //.... +} -//highlight-next-line exports.handler = verifySession(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -132,17 +119,13 @@ import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); -//highlight-start router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { - let userId = ctx.session!.getUserId(); - //highlight-end - //.... + let userId = ctx.session!.getUserId(); + //.... }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -150,105 +133,65 @@ import { verifySession } from "supertokens-node/recipe/session/framework/loopbac import { SessionContext } from "supertokens-node/framework/loopback"; class LikeComment { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @intercept(verifySession()) - @response(200) - handler() { - let userId = (this.ctx as SessionContext).session!.getUserId(); - //highlight-end - //.... - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession()) + @response(200) + handler() { + let userId = (this.ctx as SessionContext).session!.getUserId(); + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function likeComment(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) - - let userId = req.session!.getUserId(); - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let userId = session!.getUserId(); - //.... - return NextResponse.json({}) - }); + let userId = req.session!.getUserId(); + //.... } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { - //highlight-start let userId = session.getUserId(); - //highlight-end //.... return true; } } ``` - - - - - - - + + + + + + ```go import ( "fmt" @@ -273,10 +216,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -316,10 +257,8 @@ func likeCommentAPI(c *gin.Context) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -345,10 +284,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -374,57 +311,46 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session -# highlight-start -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() - # highlight-end print(user_id) ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session -# highlight-start -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session() def like_comment(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens user_id = session.get_user_id() - # highlight-end print(user_id) ``` - - - - -```python + + +```python check=false reason="session attribute is injected by framework middleware" from typing import cast from django.http import HttpRequest @@ -433,31 +359,496 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session -# highlight-start @verify_session() async def like_comment(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() - # highlight-end print(user_id) ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let userId = session!.getUserId(); + //.... + return NextResponse.json({}); + }); +} +``` + + + + + + + + +```tsx check=false reason="public interface excerpt omits dependent SDK type declarations" +interface Session { + /** + * Destroys this session in the database and on the frontend. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the session is successfully revoked. + */ + revokeSession(userContext?: Record): Promise; + + /** + * Retrieves the session data stored in the database associated with the session. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the session data. + */ + getSessionDataFromDatabase(userContext?: Record): Promise; + + /** + * Sets a new JSON object to the session data stored in the database. + * @param newSessionData The new session data to store. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the session data is updated. + */ + updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; + + /** + * Returns the user ID of the logged-in user. + * @param userContext Optional context object for additional data. + * @returns The user ID as a string. + */ + getUserId(userContext?: Record): string; + + /** + * Returns the `RecipeUserId` object for the session. It represents the user ID of the specific login method for this user. + * @param userContext Optional context object for additional data. + * @returns The `RecipeUserId`. + */ + getRecipeUserId(userContext?: Record): RecipeUserId; + + /** + * Returns the tenant ID of the session. The default value is "public" if multi-tenancy is not used. + * @param userContext Optional context object for additional data. + * @returns The tenant ID as a string. + */ + getTenantId(userContext?: Record): string; + + /** + * Returns the access token's payload for this session. This includes user-defined claims, standard claims, and SuperTokens specific ones. + * @param userContext Optional context object for additional data. + * @returns The access token payload. + */ + getAccessTokenPayload(userContext?: Record): any; + + /** + * Returns the `sessionHandle` for this session, a unique string constant for each session. + * @param userContext Optional context object for additional data. + * @returns The session handle as a string. + */ + getHandle(userContext?: Record): string; + + /** + * Returns an object containing the raw string representation of all tokens associated with the session, along with an update status. + * @returns An object with accessToken, refreshToken, antiCsrfToken, frontToken, and accessAndFrontTokenUpdated. + */ + getAllSessionTokensDangerously(): { + accessToken: string; + refreshToken: string | undefined; + antiCsrfToken: string | undefined; + frontToken: string; + accessAndFrontTokenUpdated: boolean; + }; + + /** + * Returns the raw string access token for this session. + * @param userContext Optional context object for additional data. + * @returns The access token as a string. + */ + getAccessToken(userContext?: Record): string; + + /** + * Adds key/value pairs into a JSON object in the access token. Setting a key to null removes it from the payload. + * @param accessTokenPayloadUpdate The updates to apply to the access token payload. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the payload is updated. + */ + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; + + /** + * Returns the time in milliseconds of when this session was created. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the creation time in milliseconds. + */ + getTimeCreated(userContext?: Record): Promise; + + /** + * Returns the time in milliseconds of when this session will expire if not refreshed. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the expiry time in milliseconds. + */ + getExpiry(userContext?: Record): Promise; + + /** + * Asserts the validity of custom session claims using provided validators. + * @param claimValidators An array of session claim validators. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim assertions are complete. + */ + assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; + + /** + * Fetches and sets a custom claim in the session. + * @param claim The session claim to fetch and set. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the fetched claim. + */ + fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Sets the value of a session claim. + * @param claim The session claim to update. + * @param value The new value for the claim. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim value is set. + */ + setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; + + /** + * Gets the value of a session claim. + * @param claim The session claim to retrieve the value for. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the claim value, or undefined if not found. + */ + getClaimValue(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Removes a session claim. + * @param claim The session claim to remove. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim is removed. + */ + removeClaim(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Attaches the session to a request-response cycle. + * @param reqResInfo Information about the request-response. + * @param userContext Optional context object for additional data. + * @returns A promise or void once the session is attached. + */ + attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record): Promise | void; +} +``` + + +```go check=false reason="public interface excerpt omits dependent SDK types" + + +type TypeSessionContainer struct { + // Destroys this session in the database and on the frontend. + RevokeSession func() error + + // Retrieves the session data stored in the database associated with the session. + GetSessionDataInDatabase func() (map[string]interface{}, error) + + // Sets a new JSON object to the session data stored in the database. + // `newSessionData` is the new session data to store. + UpdateSessionDataInDatabase func(newSessionData map[string]interface{}) error + + // Returns the user ID of the logged-in user. + GetUserID func() string + + // Returns the tenant ID of the session. + // Default value is "public" if multi-tenancy is not used. + GetTenantId func() string + + // Returns the access token's payload for this session. + // Includes user-defined claims, standard claims, and SuperTokens specific ones. + GetAccessTokenPayload func() map[string]interface{} + + // Returns the `sessionHandle` for this session, + // a unique string constant for each session. + GetHandle func() string + + // Returns an object containing the raw string representation + // of all tokens associated with the session, along with an update status. + GetAllSessionTokensDangerously func() SessionTokens + + // Returns the raw string access token for this session. + GetAccessToken func() string + + // Returns the time in milliseconds of when this session was created. + GetTimeCreated func() (uint64, error) + + // Returns the time in milliseconds of when this session will expire if not refreshed. + GetExpiry func() (uint64, error) + + // Context-aware methods that provide the same functionality as their counterparts above while considering user context + + // Destroys this session in the database and on the frontend with user context. + RevokeSessionWithContext func(userContext supertokens.UserContext) error + + // Retrieves the session data stored in the database associated with the session with user context. + GetSessionDataInDatabaseWithContext func(userContext supertokens.UserContext) (map[string]interface{}, error) + + // Sets a new JSON object to the session data stored in the database with user context. + UpdateSessionDataInDatabaseWithContext func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error + + // Returns the user ID of the logged-in user with user context. + GetUserIDWithContext func(userContext supertokens.UserContext) string + + // Returns the tenant ID of the session with user context. + GetTenantIdWithContext func(userContext supertokens.UserContext) string + + // Returns the access token's payload for this session with user context. + GetAccessTokenPayloadWithContext func(userContext supertokens.UserContext) map[string]interface{} + + // Returns the `sessionHandle` for this session with user context. + GetHandleWithContext func(userContext supertokens.UserContext) string + + // Returns the raw string access token for this session with user context. + GetAccessTokenWithContext func(userContext supertokens.UserContext) string + + // Returns the time in milliseconds of when this session was created with user context. + GetTimeCreatedWithContext func(userContext supertokens.UserContext) (uint64, error) + + // Returns the time in milliseconds of when this session will expire if not refreshed with user context. + GetExpiryWithContext func(userContext supertokens.UserContext) (uint64, error) + + // Adds key/value pairs into a JSON object in the access token with user context. + // Setting a key to nil removes it from the payload. + MergeIntoAccessTokenPayloadWithContext func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error + + // Asserts the validity of custom session claims using provided validators with user context. + AssertClaimsWithContext func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error + + // Fetches and sets a custom claim in the session with user context. + FetchAndSetClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error + + // Sets the value of a session claim with user context. + SetClaimValueWithContext func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error + + // Gets the value of a session claim with user context. + // Returns the value or nil if not found. + GetClaimValueWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} + + // Removes a session claim with user context. + RemoveClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error + + // Attaches the session to a request-response cycle with user context. + AttachToRequestResponseWithContext func(info RequestResponseInfo, userContext supertokens.UserContext) error -## The `Session` object + // Adds key/value pairs into a JSON object in the access token. + // Setting a key to nil removes it from the payload. + MergeIntoAccessTokenPayload func(accessTokenPayloadUpdate map[string]interface{}) error - + // Asserts the validity of custom session claims using provided validators. + AssertClaims func(claimValidators []claims.SessionClaimValidator) error + // Fetches and sets a custom claim in the session. + FetchAndSetClaim func(claim *claims.TypeSessionClaim) error + + // Sets the value of a session claim. + SetClaimValue func(claim *claims.TypeSessionClaim, value interface{}) error + + // Gets the value of a session claim. + // Returns the value or nil if not found. + GetClaimValue func(claim *claims.TypeSessionClaim) interface{} + + // Removes a session claim. + RemoveClaim func(claim *claims.TypeSessionClaim) error + + // Attaches the session to a request-response cycle. + AttachToRequestResponse func(info RequestResponseInfo) error +} +``` + + +```python +# exclude-from-type-checking + +class Session: + # Destroys this session in the database and on the frontend. + # Optional user_context can be used for additional contextual data. + async def revoke_session(self, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Retrieves the session data stored in the database associated with the session. + # Optional user_context can be used for additional contextual data. + async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + pass + + # Sets a new JSON object to the session data stored in the database. + # `new_session_data` is the new session data to store. + # Optional user_context can be used for additional contextual data. + async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Returns the user ID of the logged-in user. + # Optional user_context can be used for additional contextual data. + def get_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns the `RecipeUserId` object for the session. + # This represents the user ID of the specific login method for this user. + # Optional user_context can be used for additional contextual data. + def get_recipe_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> RecipeUserId: + pass + + # Returns the tenant ID of the session. + # Default value is "public" if multi-tenancy is not used. + # Optional user_context can be used for additional contextual data. + def get_tenant_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns the access token's payload for this session. + # Includes user-defined claims, standard claims, and SuperTokens specific ones. + # Optional user_context can be used for additional contextual data. + def get_access_token_payload(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + pass + + # Returns the `sessionHandle` for this session, + # a unique string constant for each session. + # Optional user_context can be used for additional contextual data. + def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns an object containing the raw string representation + # of all tokens associated with the session, along with an update status. + def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: + pass + + # Returns the raw string access token for this session. + # Optional user_context can be used for additional contextual data. + def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Adds key/value pairs into a JSON object in the access token. + # Setting a key to None removes it from the payload. + # `access_token_payload_update` contains the updates to apply. + # Optional user_context can be used for additional contextual data. + async def merge_into_access_token_payload(self, access_token_payload_update: JSONObject, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Returns the time in milliseconds of when this session was created. + # Optional user_context can be used for additional contextual data. + async def get_time_created(self, user_context: Optional[Dict[str, Any]] = None) -> int: + pass + + # Returns the time in milliseconds of when this session will expire if not refreshed. + # Optional user_context can be used for additional contextual data. + async def get_expiry(self, user_context: Optional[Dict[str, Any]] = None) -> int: + pass + + # Asserts the validity of custom session claims using provided validators. + # `claim_validators` is an array of session claim validators. + # Optional user_context can be used for additional contextual data. + async def assert_claims(self, claim_validators: List[SessionClaimValidator], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Fetches and sets a custom claim in the session. + # `claim` is the session claim to fetch and set. + # Optional user_context can be used for additional contextual data. + async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Sets the value of a session claim. + # `claim` is the session claim to update. + # `value` is the new value for the claim. + # Optional user_context can be used for additional contextual data. + async def set_claim_value(self, claim: SessionClaim[_T], value: _T, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Gets the value of a session claim. + # `claim` is the session claim to retrieve the value for. + # Optional user_context can be used for additional contextual data. + # Returns a promise that resolves with the claim value, or None if not found. + async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) -> Union[_T, None]: + pass + + # Removes a session claim. + # `claim` is the session claim to remove. + # Optional user_context can be used for additional contextual data. + async def remove_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Attaches the session to a request-response cycle. + # `req_res_info` contains information about the request-response. + # user_context provides contextual data for request processing. + async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod, user_context: Dict[str, Any]) -> None: + pass +``` + + + + + +
+ +### `getSessionDataFromDatabase` vs `getAccessTokenPayload` + +
+ +| | `getSessionDataFromDatabase` | `getAccessTokenPayload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `updateSessionDataInDatabase` | Updated via `mergeIntoAccessTokenPayload` | +
+ +
+ +### `GetSessionDataFromDatabase` vs `GetAccessTokenPayload` + +
+ +| | `GetSessionDataFromDatabase` | `GetAccessTokenPayload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `UpdateSessionDataInDatabase` | Updated via `MergeIntoAccessTokenPayload` | +
+ +
+ +### `get_session_data_from_database` vs `get_access_token_payload` + +
+ +| | `get_session_data_from_database` | `get_access_token_payload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `update_session_data_in_database` | Updated via `merge_into_access_token_payload` | +
+
+
@@ -465,11 +856,20 @@ async def like_comment(request: HttpRequest): To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -477,22 +877,16 @@ import { SessionRequest } from "supertokens-node/framework/express"; let app = express(); -app.post("/like-comment", - // highlight-next-line - verifySession({sessionRequired: false}), - (req: SessionRequest, res) => { - if (req.session !== undefined) { - let userId = req.session.getUserId(); - } else { - // user is not logged in... - } - } -); +app.post("/like-comment", verifySession({ sessionRequired: false }), (req: SessionRequest, res) => { + if (req.session !== undefined) { + let userId = req.session.getUserId(); + } else { + // user is not logged in... + } +}); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -501,28 +895,26 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - options: { - pre: [ - { - // highlight-next-line - method: verifySession({ sessionRequired: false }) - }, - ], - }, - handler: async (req: SessionRequest, res) => { - if (req.session !== undefined) { - let userId = req.session.getUserId(); - } else { - // user is not logged in... - } + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession({ sessionRequired: false }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + if (req.session !== undefined) { + let userId = req.session.getUserId(); + } else { + // user is not logged in... } -}) + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -530,40 +922,38 @@ import { SessionRequest } from "supertokens-node/framework/fastify"; let fastify = Fastify(); -fastify.post("/like-comment", { - // highlight-next-line +fastify.post( + "/like-comment", + { preHandler: verifySession({ sessionRequired: false }), -}, (req: SessionRequest, res) => { + }, + (req: SessionRequest, res) => { if (req.session !== undefined) { - let userId = req.session.getUserId(); + let userId = req.session.getUserId(); } else { - // user is not logged in... + // user is not logged in... } -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { - if (awsEvent.session !== undefined) { - let userId = awsEvent.session.getUserId(); - } else { - // user is not logged in... - } -}; + if (awsEvent.session !== undefined) { + let userId = awsEvent.session.getUserId(); + } else { + // user is not logged in... + } +} -// highlight-next-line exports.handler = verifySession(likeComment, { sessionRequired: false }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -571,22 +961,16 @@ import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); -router.post("/like-comment", - // highlight-next-line - verifySession({ sessionRequired: false }), - (ctx: SessionContext, next) => { - if (ctx.session !== undefined) { - let userId = ctx.session.getUserId(); - } else { - // user is not logged in... - } - } -); +router.post("/like-comment", verifySession({ sessionRequired: false }), (ctx: SessionContext, next) => { + if (ctx.session !== undefined) { + let userId = ctx.session.getUserId(); + } else { + // user is not logged in... + } +}); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -595,127 +979,81 @@ import Session from "supertokens-node/recipe/session"; import { SessionContext } from "supertokens-node/framework/loopback"; class LikeComment { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - // highlight-next-line - @intercept(verifySession({ sessionRequired: false })) - @response(200) - handler() { - let session = (this.ctx as SessionContext).session; - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession({ sessionRequired: false })) + @response(200) + handler() { + let session = (this.ctx as SessionContext).session; + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function likeComment(req: any, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ sessionRequired: false })(req, res, next); - }, - req, - res - ) - - let session = (req as SessionRequest).session; - - if (session !== undefined) { - let userId = session.getUserId(); - // session exists - } else { - // session doesn't exist - } - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + await superTokensNextWrapper( + async (next) => { + await verifySession({ sessionRequired: false })(req, res, next); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } + let session = (req as SessionRequest).session; - if (session !== undefined) { - let userId = session.getUserId(); - // session exists - } else { - // session doesn't exist - } - //.... - return NextResponse.json({}); - }, - { sessionRequired: false }); + if (session !== undefined) { + let userId = session.getUserId(); + // session exists + } else { + // session doesn't exist + } + //.... } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { OptionalAuthGuard } from './auth/optionalAuth.guard'; +import { OptionalAuthGuard } from "./auth/optionalAuth.guard"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new OptionalAuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { - //highlight-start if (session !== undefined) { - let userId = session.getUserId(); - // session exists + let userId = session.getUserId(); + // session exists } else { - // session doesn't exist + // session doesn't exist } - //highlight-end //.... return true; } } ``` - - - - - - - + + + + + + ```go import ( "fmt" @@ -731,7 +1069,6 @@ func main() { // Wrap the API handler in session.VerifySession sessionRequired := false session.VerifySession(&sessmodels.VerifySessionOptions{ - //highlight-next-line SessionRequired: &sessionRequired, }, likeCommentAPI).ServeHTTP(rw, r) }) @@ -746,10 +1083,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -766,7 +1101,6 @@ func main() { // Wrap the API handler in session.VerifySession sessionRequired := false router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ - //highlight-next-line SessionRequired: &sessionRequired, }), likeCommentAPI) } @@ -793,10 +1127,8 @@ func likeCommentAPI(c *gin.Context) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -813,7 +1145,6 @@ func main() { // Wrap the API handler in session.VerifySession sessionRequired := false r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - //highlight-next-line SessionRequired: &sessionRequired, }, likeCommentAPI)) } @@ -827,10 +1158,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -847,7 +1176,6 @@ func main() { // Wrap the API handler in session.VerifySession sessionRequired := false router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - //highlight-next-line SessionRequired: &sessionRequired, }, likeCommentAPI)).Methods(http.MethodPost) } @@ -861,16 +1189,13 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from typing import Optional from fastapi import Depends @@ -879,8 +1204,7 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session -# highlight-start -@app.post("/like_comment") # type: ignore +@app.post("/like_comment") async def like_comment( session: Optional[SessionContainer] = Depends( verify_session(session_required=False) @@ -891,13 +1215,10 @@ async def like_comment( print(user_id) # TODO.. else: pass # user is not logged in - # highlight-end ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from typing import Union from flask import g @@ -906,24 +1227,20 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session -# highlight-start -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session(session_required=False) def like_comment(): - session: Union[SessionContainer, None] = g.supertokens # type: ignore + session: Union[SessionContainer, None] = g.supertokens if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in - # highlight-end ``` - - - - -```python + + +```python check=false reason="session attribute is injected by framework middleware" from typing import Optional, cast from django.http import HttpRequest @@ -932,24 +1249,62 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session -# highlight-start @verify_session(session_required=False) async def like_comment(request: HttpRequest): - session: Optional[SessionContainer] = cast(Optional[SessionContainer], request.supertokens) # type: ignore + session: Optional[SessionContainer] = cast(Optional[SessionContainer], request.supertokens) if session is not None: user_id = session.get_user_id() print(user_id) # TODO.. else: pass # user is not logged in - # highlight-end ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } - - + if (session !== undefined) { + let userId = session.getUserId(); + // session exists + } else { + // session doesn't exist + } + //.... + return NextResponse.json({}); + }, + { sessionRequired: false }, + ); +} +``` - - + + + + + ### Verify the claims of a session @@ -960,11 +1315,20 @@ The `global validators` represent other validators that apply to all API routes This may include things like a validator that ensures that the user's email is verified. - - - - - + + + + + + + + + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; @@ -974,54 +1338,52 @@ import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( - "/update-blog", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], - }), - async (req: SessionRequest, res) => { - // All validator checks have passed and the user is an admin. - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + async (req: SessionRequest, res) => { + // All validator checks have passed and the user is an admin. + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], - }), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // All validator checks have passed and the user is an admin. - } -}) + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // All validator checks have passed and the user is an admin. + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -1030,65 +1392,67 @@ import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], }), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; async function updateBlog(awsEvent: SessionEvent) { - // All validator checks have passed and the user is an admin. -}; + // All validator checks have passed and the user is an admin. +} exports.handler = verifySession(updateBlog, { - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import {SessionContext} from "supertokens-node/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - }), async (ctx: SessionContext, next) => { +router.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -1097,123 +1461,84 @@ import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class SetRole { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })) - @response(200) - async handler() { - // All validator checks have passed and the user is an admin. - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + ) + @response(200) + async handler() { + // All validator checks have passed and the user is an admin. + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; -// highlight-start export default async function setRole(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })(req, res, next); - }, - req, - res - ) - // All validator checks have passed and the user is an admin. -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserRoles from "supertokens-node/recipe/userroles"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // All validator checks have passed and the user is an admin. - return NextResponse.json({}) + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + })(req, res, next); }, - { - // highlight-start - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] - } - // highlight-end - }); + req, + res, + ); + // All validator checks have passed and the user is an admin. } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ - ...globalValidators, + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ + ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })) + ], + }), + ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` - - - - - - - - + + + + + + ```go import ( "net/http" @@ -1228,12 +1553,10 @@ import ( func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI).ServeHTTP(rw, r) }) } @@ -1242,10 +1565,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` - - - - + + ```go import ( "net/http" @@ -1263,12 +1584,10 @@ func main() { // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }), exampleAPI) } @@ -1289,10 +1608,8 @@ func exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` - - - - + + ```go import ( "net/http" @@ -1310,12 +1627,10 @@ func main() { // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)) } @@ -1324,10 +1639,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { } ``` - - - - + + ```go import ( "net/http" @@ -1345,12 +1658,10 @@ func main() { // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)).Methods(http.MethodPost) } @@ -1358,16 +1669,13 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer @@ -1375,44 +1683,36 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) )): # All validator checks have passed and the user has a verified email address pass ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` - - - - + + ```python from django.http import HttpRequest @@ -1421,66 +1721,108 @@ from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // All validator checks have passed and the user is an admin. + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }, + ); +} +``` - - + + + + + -:::tip Feature -You can also [build your own custom claim validators](/docs/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. +:::tip[Feature] +You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: --- ## Using `Get Session` -The `Get Session` function does the same thing as the middleware, but it does not write to the client on its own. -It throws errors that you can catch and handle. +The `Get Session` function performs the same verification as the middleware, but it does not complete error responses on +its own. It can still attach updated access, front, or anti-CSRF tokens to the supplied response. It throws errors that you +can catch and handle. If these errors remain unhandled, the SuperTokens error handler catches these errors and writes to the client (like the `verifySession` middleware). You should use this function if your framework does not support middlewares or if you want additional control over error management. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; let app = express(); -// highlight-start app.post("/like-comment", async (req, res, next) => { - try { - let session = await Session.getSession(req, res); + try { + let session = await Session.getSession(req, res); - let userId = session.getUserId(); - // highlight-end - //.... - } catch (err) { - next(err); - } + let userId = session.getUserId(); + //.... + } catch (err) { + next(err); + } }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; @@ -1488,158 +1830,107 @@ import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - //highlight-start - handler: async (req, res) => { - let session = await Session.getSession(req, res); + path: "/like-comment", + method: "post", + handler: async (req, res) => { + let session = await Session.getSession(req, res); - let userId = session.getUserId(); - //highlight-end - //... - } -}) + let userId = session.getUserId(); + //... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); -//highlight-start fastify.post("/like-comment", async (req, res) => { - let session = await Session.getSession(req, res); + let session = await Session.getSession(req, res); - let userId = session.getUserId(); - //highlight-end - //.... + let userId = session.getUserId(); + //.... }); ``` - - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; -//highlight-start async function likeComment(awsEvent: SessionEvent) { - let session = await Session.getSession(awsEvent, awsEvent); + let session = await Session.getSession(awsEvent, awsEvent); - let userId = session.getUserId(); - //highlight-end - //.... -}; + let userId = session.getUserId(); + //.... +} -//highlight-next-line exports.handler = middleware(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); -//highlight-start router.post("/like-comment", async (ctx, next) => { - let session = await Session.getSession(ctx, ctx); + let session = await Session.getSession(ctx, ctx); - let userId = session.getUserId(); - //highlight-end - //.... + let userId = session.getUserId(); + //.... }); ``` - - - - + + ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @response(200) - async handler() { - let session = await Session.getSession(this.ctx, this.ctx); + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @response(200) + async handler() { + let session = await Session.getSession(this.ctx, this.ctx); - let userId = session.getUserId(); - //highlight-end - //.... - } + let userId = session.getUserId(); + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function likeComment(req: SessionRequest, res: any) { - let session = await superTokensNextWrapper( - async (next) => { - return await Session.getSession(req, res); - }, - req, - res - ) - - let userId = session.getUserId(); - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; -import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; -import Session from "supertokens-node/recipe/session"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + let session = await superTokensNextWrapper( + async (next) => { + return await Session.getSession(req, res); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { - const session = await Session.getSession(baseRequest, baseResponse); - let userId = session.getUserId(); - return NextResponse.json({}); - }); + let userId = session.getUserId(); + //.... } ``` - - - - + + ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; @@ -1647,24 +1938,21 @@ import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') - async postExample(@Req() req: Request, @Res({passthrough: true}) res: Response): Promise { - //highlight-start + @Post("example") + async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res); const userId = session.getUserId(); - //highlight-end //.... return true; } } ``` - - - - - + + + + ```go import ( "fmt" @@ -1691,22 +1979,18 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - - - - -```python + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi.requests import Request from supertokens_python.recipe.session.asyncio import get_session -@app.post('/like-comment') # type: ignore +@app.post('/like-comment') async def like_comment(request: Request): - # highlight-next-line session = await get_session(request) if session is None: @@ -1717,19 +2001,16 @@ async def like_comment(request: Request): print(user_id) # TODO ``` - - - - -```python -from flask.wrappers import Request + + +```python check=false reason="route fragment assumes an existing framework application" +from flask import request from supertokens_python.recipe.session.syncio import get_session -@app.route('/like-comment', methods=['POST']) # type: ignore -def like_comment(request: Request): - # highlight-next-line +@app.route('/like-comment', methods=['POST']) +def like_comment(): session = get_session(request) if session is None: @@ -1740,10 +2021,8 @@ def like_comment(request: Request): print(user_id) # TODO ``` - - - - + + ```python from django.http import HttpRequest @@ -1751,7 +2030,6 @@ from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): - # highlight-next-line session = await get_session(request) if session is None: raise Exception("Should never come here") @@ -1761,30 +2039,509 @@ async def like_comment(request: HttpRequest): print(user_id) # TODO ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; +import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; +import Session from "supertokens-node/recipe/session"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withPreParsedRequestResponse( + request, + async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { + const session = await Session.getSession(baseRequest, baseResponse); + let userId = session.getUserId(); + return NextResponse.json({}); + }, + ); +} +``` - - + + + + + + + + +```tsx check=false reason="public interface excerpt omits dependent SDK type declarations" +interface Session { + /** + * Destroys this session in the database and on the frontend. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the session is successfully revoked. + */ + revokeSession(userContext?: Record): Promise; + + /** + * Retrieves the session data stored in the database associated with the session. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the session data. + */ + getSessionDataFromDatabase(userContext?: Record): Promise; + + /** + * Sets a new JSON object to the session data stored in the database. + * @param newSessionData The new session data to store. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the session data is updated. + */ + updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; + + /** + * Returns the user ID of the logged-in user. + * @param userContext Optional context object for additional data. + * @returns The user ID as a string. + */ + getUserId(userContext?: Record): string; + + /** + * Returns the `RecipeUserId` object for the session. It represents the user ID of the specific login method for this user. + * @param userContext Optional context object for additional data. + * @returns The `RecipeUserId`. + */ + getRecipeUserId(userContext?: Record): RecipeUserId; + + /** + * Returns the tenant ID of the session. The default value is "public" if multi-tenancy is not used. + * @param userContext Optional context object for additional data. + * @returns The tenant ID as a string. + */ + getTenantId(userContext?: Record): string; + + /** + * Returns the access token's payload for this session. This includes user-defined claims, standard claims, and SuperTokens specific ones. + * @param userContext Optional context object for additional data. + * @returns The access token payload. + */ + getAccessTokenPayload(userContext?: Record): any; + + /** + * Returns the `sessionHandle` for this session, a unique string constant for each session. + * @param userContext Optional context object for additional data. + * @returns The session handle as a string. + */ + getHandle(userContext?: Record): string; + + /** + * Returns an object containing the raw string representation of all tokens associated with the session, along with an update status. + * @returns An object with accessToken, refreshToken, antiCsrfToken, frontToken, and accessAndFrontTokenUpdated. + */ + getAllSessionTokensDangerously(): { + accessToken: string; + refreshToken: string | undefined; + antiCsrfToken: string | undefined; + frontToken: string; + accessAndFrontTokenUpdated: boolean; + }; + + /** + * Returns the raw string access token for this session. + * @param userContext Optional context object for additional data. + * @returns The access token as a string. + */ + getAccessToken(userContext?: Record): string; + + /** + * Adds key/value pairs into a JSON object in the access token. Setting a key to null removes it from the payload. + * @param accessTokenPayloadUpdate The updates to apply to the access token payload. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the payload is updated. + */ + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; + + /** + * Returns the time in milliseconds of when this session was created. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the creation time in milliseconds. + */ + getTimeCreated(userContext?: Record): Promise; + + /** + * Returns the time in milliseconds of when this session will expire if not refreshed. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the expiry time in milliseconds. + */ + getExpiry(userContext?: Record): Promise; + + /** + * Asserts the validity of custom session claims using provided validators. + * @param claimValidators An array of session claim validators. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim assertions are complete. + */ + assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; + + /** + * Fetches and sets a custom claim in the session. + * @param claim The session claim to fetch and set. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the fetched claim. + */ + fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Sets the value of a session claim. + * @param claim The session claim to update. + * @param value The new value for the claim. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim value is set. + */ + setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; + + /** + * Gets the value of a session claim. + * @param claim The session claim to retrieve the value for. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves with the claim value, or undefined if not found. + */ + getClaimValue(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Removes a session claim. + * @param claim The session claim to remove. + * @param userContext Optional context object for additional data. + * @returns A promise that resolves when the claim is removed. + */ + removeClaim(claim: SessionClaim, userContext?: Record): Promise; + + /** + * Attaches the session to a request-response cycle. + * @param reqResInfo Information about the request-response. + * @param userContext Optional context object for additional data. + * @returns A promise or void once the session is attached. + */ + attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record): Promise | void; +} +``` + + +```go check=false reason="public interface excerpt omits dependent SDK types" + + +type TypeSessionContainer struct { + // Destroys this session in the database and on the frontend. + RevokeSession func() error + + // Retrieves the session data stored in the database associated with the session. + GetSessionDataInDatabase func() (map[string]interface{}, error) + + // Sets a new JSON object to the session data stored in the database. + // `newSessionData` is the new session data to store. + UpdateSessionDataInDatabase func(newSessionData map[string]interface{}) error + + // Returns the user ID of the logged-in user. + GetUserID func() string + + // Returns the tenant ID of the session. + // Default value is "public" if multi-tenancy is not used. + GetTenantId func() string + + // Returns the access token's payload for this session. + // Includes user-defined claims, standard claims, and SuperTokens specific ones. + GetAccessTokenPayload func() map[string]interface{} + + // Returns the `sessionHandle` for this session, + // a unique string constant for each session. + GetHandle func() string + + // Returns an object containing the raw string representation + // of all tokens associated with the session, along with an update status. + GetAllSessionTokensDangerously func() SessionTokens + + // Returns the raw string access token for this session. + GetAccessToken func() string + + // Returns the time in milliseconds of when this session was created. + GetTimeCreated func() (uint64, error) + + // Returns the time in milliseconds of when this session will expire if not refreshed. + GetExpiry func() (uint64, error) + + // Context-aware methods that provide the same functionality as their counterparts above while considering user context + + // Destroys this session in the database and on the frontend with user context. + RevokeSessionWithContext func(userContext supertokens.UserContext) error + + // Retrieves the session data stored in the database associated with the session with user context. + GetSessionDataInDatabaseWithContext func(userContext supertokens.UserContext) (map[string]interface{}, error) + + // Sets a new JSON object to the session data stored in the database with user context. + UpdateSessionDataInDatabaseWithContext func(newSessionData map[string]interface{}, userContext supertokens.UserContext) error + + // Returns the user ID of the logged-in user with user context. + GetUserIDWithContext func(userContext supertokens.UserContext) string + + // Returns the tenant ID of the session with user context. + GetTenantIdWithContext func(userContext supertokens.UserContext) string + + // Returns the access token's payload for this session with user context. + GetAccessTokenPayloadWithContext func(userContext supertokens.UserContext) map[string]interface{} + + // Returns the `sessionHandle` for this session with user context. + GetHandleWithContext func(userContext supertokens.UserContext) string + + // Returns the raw string access token for this session with user context. + GetAccessTokenWithContext func(userContext supertokens.UserContext) string + + // Returns the time in milliseconds of when this session was created with user context. + GetTimeCreatedWithContext func(userContext supertokens.UserContext) (uint64, error) + + // Returns the time in milliseconds of when this session will expire if not refreshed with user context. + GetExpiryWithContext func(userContext supertokens.UserContext) (uint64, error) + + // Adds key/value pairs into a JSON object in the access token with user context. + // Setting a key to nil removes it from the payload. + MergeIntoAccessTokenPayloadWithContext func(accessTokenPayloadUpdate map[string]interface{}, userContext supertokens.UserContext) error + + // Asserts the validity of custom session claims using provided validators with user context. + AssertClaimsWithContext func(claimValidators []claims.SessionClaimValidator, userContext supertokens.UserContext) error + + // Fetches and sets a custom claim in the session with user context. + FetchAndSetClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error + + // Sets the value of a session claim with user context. + SetClaimValueWithContext func(claim *claims.TypeSessionClaim, value interface{}, userContext supertokens.UserContext) error + + // Gets the value of a session claim with user context. + // Returns the value or nil if not found. + GetClaimValueWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) interface{} + + // Removes a session claim with user context. + RemoveClaimWithContext func(claim *claims.TypeSessionClaim, userContext supertokens.UserContext) error + + // Attaches the session to a request-response cycle with user context. + AttachToRequestResponseWithContext func(info RequestResponseInfo, userContext supertokens.UserContext) error + + // Adds key/value pairs into a JSON object in the access token. + // Setting a key to nil removes it from the payload. + MergeIntoAccessTokenPayload func(accessTokenPayloadUpdate map[string]interface{}) error + + // Asserts the validity of custom session claims using provided validators. + AssertClaims func(claimValidators []claims.SessionClaimValidator) error -## The `Session` object + // Fetches and sets a custom claim in the session. + FetchAndSetClaim func(claim *claims.TypeSessionClaim) error - + // Sets the value of a session claim. + SetClaimValue func(claim *claims.TypeSessionClaim, value interface{}) error + // Gets the value of a session claim. + // Returns the value or nil if not found. + GetClaimValue func(claim *claims.TypeSessionClaim) interface{} + + // Removes a session claim. + RemoveClaim func(claim *claims.TypeSessionClaim) error + + // Attaches the session to a request-response cycle. + AttachToRequestResponse func(info RequestResponseInfo) error +} +``` + + +```python +# exclude-from-type-checking + +class Session: + # Destroys this session in the database and on the frontend. + # Optional user_context can be used for additional contextual data. + async def revoke_session(self, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Retrieves the session data stored in the database associated with the session. + # Optional user_context can be used for additional contextual data. + async def get_session_data_from_database(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + pass + + # Sets a new JSON object to the session data stored in the database. + # `new_session_data` is the new session data to store. + # Optional user_context can be used for additional contextual data. + async def update_session_data_in_database(self, new_session_data: Dict[str, Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Returns the user ID of the logged-in user. + # Optional user_context can be used for additional contextual data. + def get_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns the `RecipeUserId` object for the session. + # This represents the user ID of the specific login method for this user. + # Optional user_context can be used for additional contextual data. + def get_recipe_user_id(self, user_context: Optional[Dict[str, Any]] = None) -> RecipeUserId: + pass + + # Returns the tenant ID of the session. + # Default value is "public" if multi-tenancy is not used. + # Optional user_context can be used for additional contextual data. + def get_tenant_id(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns the access token's payload for this session. + # Includes user-defined claims, standard claims, and SuperTokens specific ones. + # Optional user_context can be used for additional contextual data. + def get_access_token_payload(self, user_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + pass + + # Returns the `sessionHandle` for this session, + # a unique string constant for each session. + # Optional user_context can be used for additional contextual data. + def get_handle(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Returns an object containing the raw string representation + # of all tokens associated with the session, along with an update status. + def get_all_session_tokens_dangerously(self) -> GetSessionTokensDangerouslyDict: + pass + + # Returns the raw string access token for this session. + # Optional user_context can be used for additional contextual data. + def get_access_token(self, user_context: Optional[Dict[str, Any]] = None) -> str: + pass + + # Adds key/value pairs into a JSON object in the access token. + # Setting a key to None removes it from the payload. + # `access_token_payload_update` contains the updates to apply. + # Optional user_context can be used for additional contextual data. + async def merge_into_access_token_payload(self, access_token_payload_update: JSONObject, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Returns the time in milliseconds of when this session was created. + # Optional user_context can be used for additional contextual data. + async def get_time_created(self, user_context: Optional[Dict[str, Any]] = None) -> int: + pass + + # Returns the time in milliseconds of when this session will expire if not refreshed. + # Optional user_context can be used for additional contextual data. + async def get_expiry(self, user_context: Optional[Dict[str, Any]] = None) -> int: + pass + + # Asserts the validity of custom session claims using provided validators. + # `claim_validators` is an array of session claim validators. + # Optional user_context can be used for additional contextual data. + async def assert_claims(self, claim_validators: List[SessionClaimValidator], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Fetches and sets a custom claim in the session. + # `claim` is the session claim to fetch and set. + # Optional user_context can be used for additional contextual data. + async def fetch_and_set_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Sets the value of a session claim. + # `claim` is the session claim to update. + # `value` is the new value for the claim. + # Optional user_context can be used for additional contextual data. + async def set_claim_value(self, claim: SessionClaim[_T], value: _T, user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Gets the value of a session claim. + # `claim` is the session claim to retrieve the value for. + # Optional user_context can be used for additional contextual data. + # Returns a promise that resolves with the claim value, or None if not found. + async def get_claim_value(self, claim: SessionClaim[_T], user_context: Optional[Dict[str, Any]] = None) -> Union[_T, None]: + pass + + # Removes a session claim. + # `claim` is the session claim to remove. + # Optional user_context can be used for additional contextual data. + async def remove_claim(self, claim: SessionClaim[Any], user_context: Optional[Dict[str, Any]] = None) -> None: + pass + + # Attaches the session to a request-response cycle. + # `req_res_info` contains information about the request-response. + # user_context provides contextual data for request processing. + async def attach_to_request_response(self, request: BaseRequest, transfer_method: TokenTransferMethod, user_context: Dict[str, Any]) -> None: + pass +``` + + + + + +
+ +### `getSessionDataFromDatabase` vs `getAccessTokenPayload` + +
+ +| | `getSessionDataFromDatabase` | `getAccessTokenPayload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `updateSessionDataInDatabase` | Updated via `mergeIntoAccessTokenPayload` | +
+ +
+ +### `GetSessionDataFromDatabase` vs `GetAccessTokenPayload` + +
+ +| | `GetSessionDataFromDatabase` | `GetAccessTokenPayload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `UpdateSessionDataInDatabase` | Updated via `MergeIntoAccessTokenPayload` | +
+ +
+ +### `get_session_data_from_database` vs `get_access_token_payload` + +
+ +| | `get_session_data_from_database` | `get_access_token_payload` | +|--------------------------------------|-----------------------------|-------------------------| +| **Source of Data** | Queries SuperTokens Core database | Reads directly from the access token in the request | +| **Speed** | Slower (requires a network call) | Faster (no network call required) | +| **Data Sensitivity** | Secure—data is not exposed to the frontend | The access token includes data, which is accessible to the frontend | +| **Use Case** | Best for storing sensitive session-related data | Best for frequently accessed data like user roles | +| **Persistence** | Updated via `update_session_data_in_database` | Updated via `merge_into_access_token_payload` | +
+
+
### Optional session verification To make an API endpoint accessible even if there is no session update the middleware call to mark the session as not required. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; @@ -1792,24 +2549,22 @@ import Session from "supertokens-node/recipe/session"; let app = express(); app.post("/like-comment", async (req, res, next) => { - try { - let session = await Session.getSession(req, res, { sessionRequired: false }) + try { + let session = await Session.getSession(req, res, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - //.... - } catch (err) { - next(err); + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } + //.... + } catch (err) { + next(err); + } }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; @@ -1817,25 +2572,24 @@ import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", + path: "/like-comment", + method: "post", - handler: async (req, res) => { - let session = await Session.getSession(req, res, { sessionRequired: false }) + handler: async (req, res) => { + let session = await Session.getSession(req, res, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - - //... + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } -}) -``` - - + //... + }, +}); +``` + + ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; @@ -1843,44 +2597,39 @@ import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { - let session = await Session.getSession(req, res, { sessionRequired: false }) + let session = await Session.getSession(req, res, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - //.... + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } + //.... }); ``` - - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { - let session = await Session.getSession(awsEvent, awsEvent, { sessionRequired: false }) + let session = await Session.getSession(awsEvent, awsEvent, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } - //.... -}; + //.... +} -//highlight-next-line exports.handler = middleware(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; @@ -1888,109 +2637,71 @@ import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { - let session = await Session.getSession(ctx, ctx, { sessionRequired: false }) + let session = await Session.getSession(ctx, ctx, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } - //.... + //.... }); ``` - - - - + + ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { - - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @response(200) - async handler() { - let session = await Session.getSession(this.ctx, this.ctx, { sessionRequired: false }) - - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - - //.... - } -} -``` - - - - - - - - - - -```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' -import Session from "supertokens-node/recipe/session"; -import { SessionRequest } from "supertokens-node/framework/express"; - -export default async function likeComment(req: SessionRequest, res: any) { - let session = await superTokensNextWrapper( - async (next) => { - return await Session.getSession(req, res, { sessionRequired: false }); - }, - req, - res - ) + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @response(200) + async handler() { + let session = await Session.getSession(this.ctx, this.ctx, { sessionRequired: false }); if (session !== undefined) { - let userId = session.getUserId(); + let userId = session.getUserId(); } else { - // user is not logged in... + // user is not logged in... } + //.... + } } ``` - - - + + + ```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; -import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; +import { SessionRequest } from "supertokens-node/framework/express"; -SuperTokens.init(backendConfig()); +export default async function likeComment(req: SessionRequest, res: any) { + let session = await superTokensNextWrapper( + async (next) => { + return await Session.getSession(req, res, { sessionRequired: false }); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { - const session = await Session.getSession(baseRequest, baseResponse, { sessionRequired: false }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - return NextResponse.json({}); - }); + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } + //.... } ``` - - - - + + ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; @@ -1998,28 +2709,25 @@ import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') + @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { - //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. - const session = await Session.getSession(req, res, { sessionRequired: false }) + const session = await Session.getSession(req, res, { sessionRequired: false }); if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } - //highlight-end //.... return true; } } ``` - - - - - + + + + ```go import ( "fmt" @@ -2052,21 +2760,18 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { } } ``` - - - - - - -```python + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Request from supertokens_python.recipe.session.asyncio import get_session -@app.post("/like-comment") # type: ignore +@app.post("/like-comment") async def like_comment(request: Request): - # highlight-next-line session = await get_session(request, session_required=False) if session is not None: @@ -2076,19 +2781,16 @@ async def like_comment(request: Request): pass # user is not logged in ``` - - - - -```python -from flask.wrappers import Request + + +```python check=false reason="route fragment assumes an existing framework application" +from flask import request from supertokens_python.recipe.session.syncio import get_session -@app.route("/like-comment", methods=["POST"]) # type: ignore -def like_comment(request: Request): - # highlight-next-line +@app.route("/like-comment", methods=["POST"]) +def like_comment(): session = get_session(request, session_required=False) if session is not None: @@ -2098,10 +2800,8 @@ def like_comment(request: Request): pass # user is not logged in ``` - - - - + + ```python from django.http import HttpRequest @@ -2109,7 +2809,6 @@ from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): - # highlight-next-line session = await get_session(request, session_required=False) if session is not None: @@ -2119,12 +2818,48 @@ async def like_comment(request: HttpRequest): pass # user is not logged in ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; +import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; +import Session from "supertokens-node/recipe/session"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withPreParsedRequestResponse( + request, + async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { + const session = await Session.getSession(baseRequest, baseResponse, { sessionRequired: false }); + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } + return NextResponse.json({}); + }, + ); +} +``` + + + + + + ### Verify the claims of a session @@ -2134,11 +2869,20 @@ This can be achieved by including the user role claim validator in the middlewar The `global validators` represent other validators that apply to all API routes by default. This may include things like a validator that ensures that the user's email is verified. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; @@ -2146,29 +2890,25 @@ import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); -// highlight-start app.post("/like-comment", async (req, res, next) => { - try { - let session = await Session.getSession(req, res, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + try { + let session = await Session.getSession(req, res, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - // highlight-end - //.... - } catch (err) { - next(err) - } + let userId = session.getUserId(); + //.... + } catch (err) { + next(err); + } }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; @@ -2177,27 +2917,24 @@ import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - //highlight-start - handler: async (req, res) => { - let session = await Session.getSession(req, res, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + path: "/like-comment", + method: "post", + handler: async (req, res) => { + let session = await Session.getSession(req, res, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - //highlight-end - //... - } -}) + let userId = session.getUserId(); + //... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; @@ -2205,53 +2942,44 @@ import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); -//highlight-start fastify.post("/like-comment", async (req, res) => { - let session = await Session.getSession(req, res, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + let session = await Session.getSession(req, res, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - //highlight-end - //.... + let userId = session.getUserId(); + //.... }); ``` - - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; -//highlight-start async function likeComment(awsEvent: SessionEvent) { - let session = await Session.getSession(awsEvent, awsEvent, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + let session = await Session.getSession(awsEvent, awsEvent, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - //highlight-end - //.... -}; + let userId = session.getUserId(); + //.... +} -//highlight-next-line exports.handler = middleware(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; @@ -2259,25 +2987,21 @@ import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); -//highlight-start router.post("/like-comment", async (ctx, next) => { - let session = await Session.getSession(ctx, ctx, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + let session = await Session.getSession(ctx, ctx, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - //highlight-end - //.... + let userId = session.getUserId(); + //.... }); ``` - - - - + + ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -2285,98 +3009,56 @@ import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class LikeComment { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @response(200) - async handler() { - let session = await Session.getSession(this.ctx, this.ctx, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @response(200) + async handler() { + let session = await Session.getSession(this.ctx, this.ctx, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); - let userId = session.getUserId(); - //highlight-end - //.... - } + let userId = session.getUserId(); + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; -// highlight-start export default async function likeComment(req: SessionRequest, res: any) { - let session = await superTokensNextWrapper( - async (next) => { - return await Session.getSession(req, res, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); - }, - req, - res - ) - - let userId = session.getUserId(); - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; -import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; -import Session from "supertokens-node/recipe/session"; -import UserRoles from "supertokens-node/recipe/userroles"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + let session = await superTokensNextWrapper( + async (next) => { + return await Session.getSession(req, res, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withPreParsedRequestResponse(request, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { - const session = await Session.getSession(baseRequest, baseResponse, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] - }); - let userId = session.getUserId(); - return NextResponse.json({}); - }); + let userId = session.getUserId(); + //.... } ``` - - - - + + ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; @@ -2385,30 +3067,27 @@ import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { - @Post('example') - async postExample(@Req() req: Request, @Res({passthrough: true}) res: Response): Promise { - //highlight-start + @Post("example") + async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { // This should be done inside a parameter decorator, for more information please read our NestJS guide. const session = await Session.getSession(req, res, { - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ] + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], }); const userId = session.getUserId(); - //highlight-end //.... return true; } } ``` - - - - - + + + + ```go import ( "fmt" @@ -2423,12 +3102,10 @@ import ( func likeCommentAPI(w http.ResponseWriter, r *http.Request) { sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }) if err != nil { @@ -2445,21 +3122,18 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - - - - -```python + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi.requests import Request from supertokens_python.recipe.session.asyncio import get_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.post('/like-comment') # type: ignore +@app.post('/like-comment') async def like_comment(request: Request): session = await get_session(request, override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ @@ -2473,19 +3147,17 @@ async def like_comment(request: Request): print(user_id) # TODO ``` - - - - -```python -from flask.wrappers import Request + + +```python check=false reason="route fragment assumes an existing framework application" +from flask import request from supertokens_python.recipe.session.syncio import get_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.route("/like-comment", methods=["POST"]) # type: ignore -def like_comment(request: Request): +@app.route("/like-comment", methods=["POST"]) +def like_comment(): session = get_session( request, override_global_claim_validators=lambda global_validators, @@ -2502,10 +3174,8 @@ def like_comment(request: Request): # TODO ``` - - - - + + ```python from django.http import HttpRequest @@ -2528,15 +3198,54 @@ async def like_comment(request: HttpRequest): print(user_id) # TODO ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; +import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; +import Session from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withPreParsedRequestResponse( + request, + async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { + const session = await Session.getSession(baseRequest, baseResponse, { + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }); + let userId = session.getUserId(); + return NextResponse.json({}); + }, + ); +} +``` - - + + + + + -:::tip feature -You can also [build your own custom claim validators](/docs/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. +:::tip[feature] +You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: ### Build your own middleware @@ -2548,9 +3257,8 @@ It throws an exception that you can handle if the access token expires or is inv Internally, `Verify Session` uses `Get Session` in the following way: - - - + + ```tsx import { VerifySessionOptions } from "supertokens-node/recipe/session/types"; import { errorHandler } from "supertokens-node/framework/express"; @@ -2559,46 +3267,40 @@ import Session from "supertokens-node/recipe/session"; import { Error as SuperTokensError } from "supertokens-node"; function verifySession(options?: VerifySessionOptions) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - (req as any).session = await Session.getSession(req, res, options); - next(); - } catch (err) { - if (SuperTokensError.isErrorFromSuperTokens(err)) { - if (err.type === Session.Error.TRY_REFRESH_TOKEN) { - // This means that the session exists, but the access token - // has expired. - - // You can handle this in a custom way by sending a 401. - // Or you can call the errorHandler middleware as shown below - } else if (err.type === Session.Error.UNAUTHORISED) { - // This means that the session does not exist anymore. - - // You can handle this in a custom way by sending a 401. - // Or you can call the errorHandler middleware as shown below - } else if (err.type === Session.Error.INVALID_CLAIMS) { - // The user is missing some required claim. - // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. - } - - // OR you can use this errorHandler which will - // handle all of the above errors in the default way - errorHandler()(err, req, res, (err) => { - next(err) - }) - } else { - next(err) - } + return async (req: Request, res: Response, next: NextFunction) => { + try { + (req as any).session = await Session.getSession(req, res, options); + next(); + } catch (err) { + if (SuperTokensError.isErrorFromSuperTokens(err)) { + if (err.type === Session.Error.TRY_REFRESH_TOKEN) { + // This means that the session exists, but the access token + // has expired. + // You can handle this in a custom way by sending a 401. + // Or you can call the errorHandler middleware as shown below + } else if (err.type === Session.Error.UNAUTHORISED) { + // This means that the session does not exist anymore. + // You can handle this in a custom way by sending a 401. + // Or you can call the errorHandler middleware as shown below + } else if (err.type === Session.Error.INVALID_CLAIMS) { + // The user is missing some required claim. + // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. } - }; + + // OR you can use this errorHandler which will + // handle all of the above errors in the default way + errorHandler()(err, req, res, (err) => { + next(err); + }); + } else { + next(err); + } + } + }; } ``` - -The `errorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. - - - - + + ```go import ( "context" @@ -2649,12 +3351,8 @@ func VerifySession(options *sessmodels.VerifySessionOptions, otherHandler http.H }) } ``` - -The `SuperTokens.ErrorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. - - - - + + ```python from functools import wraps from typing import Any, Callable, Dict, List, Optional, TypeVar, Union, cast @@ -2676,7 +3374,7 @@ _T = TypeVar("_T", bound=Callable[..., Any]) def verify_session( session_required: bool = True, anti_csrf_check: Union[bool, None] = None, - check_database: bool = True, + check_database: Optional[bool] = None, override_global_claim_validators: Optional[ Callable[ [List[SessionClaimValidator], SessionContainer, Dict[str, Any]], @@ -2734,11 +3432,20 @@ def verify_session( return session_verify ``` + + + + +The `errorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. + + +The `SuperTokens.ErrorHandler` sends a `401` reply to the frontend if the `getSession` function throws an exception indicating that the session does not exist or if the access token has expired. + + If `get_session` throws an error (in case the input access token is invalid or has expired), then the SuperTokens middleware added to your app handles that exception. It sends a `401` to the frontend. - - - + + ### Get the session using the `Access Token` @@ -2752,9 +3459,8 @@ This function works similarly to `getSession`, except that it doesn't depend on It's your responsibility to provide this function the access token. You must write the update tokens to the response if the tokens update during this API call. - - - + + ```tsx import { VerifySessionOptions } from "supertokens-node/recipe/session/types"; import { SessionContainer } from "supertokens-node/recipe/session"; @@ -2762,50 +3468,46 @@ import Session from "supertokens-node/recipe/session"; import { Error as SuperTokensError } from "supertokens-node"; async function verifySession(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions) { - let session: SessionContainer | undefined; - try { - session = await Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options); - } catch (err) { - if (SuperTokensError.isErrorFromSuperTokens(err)) { - if (err.type === Session.Error.TRY_REFRESH_TOKEN) { - // This means that the session exists, but the access token - // has expired. - - // You can handle this in a custom way by sending a 401. - // Or you can call the errorHandler middleware as shown below - } else if (err.type === Session.Error.UNAUTHORISED) { - // This means that the session does not exist anymore. - - // You can handle this in a custom way by sending a 401. - // Or you can call the errorHandler middleware as shown below - } else if (err.type === Session.Error.INVALID_CLAIMS) { - // The user is missing some required claim. - // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. - } - } - throw err; + let session: SessionContainer | undefined; + try { + session = await Session.getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options); + } catch (err) { + if (SuperTokensError.isErrorFromSuperTokens(err)) { + if (err.type === Session.Error.TRY_REFRESH_TOKEN) { + // This means that the session exists, but the access token + // has expired. + // You can handle this in a custom way by sending a 401. + // Or you can call the errorHandler middleware as shown below + } else if (err.type === Session.Error.UNAUTHORISED) { + // This means that the session does not exist anymore. + // You can handle this in a custom way by sending a 401. + // Or you can call the errorHandler middleware as shown below + } else if (err.type === Session.Error.INVALID_CLAIMS) { + // The user is missing some required claim. + // You can pass the missing claims to the frontend and handle it there. Send a 403 to the frontend. + } } - if (session !== undefined) { - // we can use the `session` container as we usually do.. - // TODO: API logic... - - // At the end of the API logic, we must fetch all the tokens from the session container - // and set them in the response headers / cookies ourselves. - const tokens = session.getAllSessionTokensDangerously(); - if (tokens.accessAndFrontTokenUpdated) { - // TODO: set access token in response via tokens.accessToken - // TODO: set front-token in response via tokens.frontToken - if (tokens.antiCsrfToken) { - // TODO: set anti-csrf token update in response via tokens.antiCsrfToken - } - } + throw err; + } + if (session !== undefined) { + // we can use the `session` container as we usually do.. + // TODO: API logic... + + // At the end of the API logic, we must fetch all the tokens from the session container + // and set them in the response headers / cookies ourselves. + const tokens = session.getAllSessionTokensDangerously(); + if (tokens.accessAndFrontTokenUpdated) { + // TODO: set access token in response via tokens.accessToken + // TODO: set front-token in response via tokens.frontToken + if (tokens.antiCsrfToken) { + // TODO: set anti-csrf token update in response via tokens.antiCsrfToken + } } + } } ``` - - - - + + ```go import ( defaultErrors "errors" @@ -2855,10 +3557,8 @@ func VerifySession(accessToken string, antiCsrfToken *string, options *sessmodel return nil } ``` - - - - + + ```python from typing import Any, Callable, Dict, List, Optional, TypeVar @@ -2937,347 +3637,61 @@ def verify_session( pass ``` - - - + + --- -## Using a JWT verification library - -If the previous methods are not suitable for your use case, you can use validate the token manually. -This method works for cases like: -- Your APIs are on a backend for which SuperTokens doesn't have SDKs. -- You are using a non `http` protocol (like `websockets`) and passing in the access token. -- You are using an API gateway which does JWT verification based on the JWKs endpoint. - -:::caution -The downside to using JWT verification manually is that: -- Pick and configure a JWT verification library for your framework. Many online guides explain how to do this. -- You need to manually verify some custom claims in the JWT (like the user's role is `admin`, or that the user's email is verified) based on your authorization rules. -- You don't have access to the [`session` object](/docs/additional-verification/session-verification/protect-api-routes#using-verify-session) using which you can modify the session's access token payload, or revoke the session. These operations can occur in an offline manner, but they reflect in the user's session only after a session refresh. -::: - -The manual session verification method should work in this way: - - - ## Verify the JWT signature and expiry using a JWT verification library - ## Check for custom claim values for authorization. - ## Prevent cross-site request forgery (CSRF) attacks in case you are using cookies to store the JWT. - - - - -### With the JSON Web Key Set (JWKS) Endpoint - - - - -Some libraries let you provide a JSON Web Key Set (JWKS) endpoint to verify a JWT. -The JSON Web Key Set (JWKS) endpoint exposed by SuperTokens is available at the following URL: - -```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -``` - -Below is an example for NodeJS showing how you can use `jsonwebtoken` and `jwks-rsa` together to achieve JWT verification using the `jwks.json` endpoint. - -```ts -import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); - -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); -} - -let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header -JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { - let decodedJWT = decoded; - // Use JWT -}); -``` - - - - -Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa) for a code reference of how use `PyJWK` to do session verification. The gist contains two files: -- `jwt_verification.py` (which you can copy/paste into your application). You need to modify the `JWKS_URI` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). This file is for `sync` python apps and can be modified to work with `async` apps as well. - - This file essentially exposes a function called `verify_jwt` which takes an input JWT string. - - This function takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. -- `views.py`: This is an example `GET` API which extracts the JWT token from the authorization header in the request and calls the `verify_jwt` function from the other file. If you are using cookie based auth instead of header based auth, you should read the JWT from the `sAccessToken` cookie in the request. - - - - -Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d) for a code reference of how use the Golang `jwt` lib to do session verification. The gist contains two files: -- `verifyToken.go` (which you can copy/paste into your application). You need to modify the `coreUrl` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - - This file essentially exposes a function called `GetJWKS` which returns a reference to the JSON Web Key Set (JWKS) public keys usable for JWT verification. - - This function takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. -- `main.go`: This is an example of how to verify a JWT using the golang JWT verification lib along with the helper function to get the JWKs keys. If you are using header-based auth, you can fetch the JWT from the `Authorization Bearer` header, otherwise for cookie-based auth, you can fetch it from the `sAccessToken` cookie. - - - - -Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/5b2d19c02337ed7ee387723c84def9cd) for a code reference of how use the Java `nimbus-jose-jwt` lib to do session verification. The gist contains three files: -- `JWTVerification.java` You need to modify the `CORE_URL` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - - This is an example of how to verify a JWT using the Java `nimbus-jose-jwt` lib along with the helper method to get the JWKs keys. If you are using header-based auth, you can fetch the JWT from the `Authorization Bearer` header, otherwise for cookie-based auth, you can fetch it from the `sAccessToken` cookie. - - This file has a method called `setSource` which returns a reference to the JSON Web Key Set (JWKS) public keys usable for JWT verification. This method takes care of caching public keys in memory and auto re-fetching if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts and is a security feature. -- `pom.xml`: This shows the version of `nimbus-jose-jwt` used for this project. -- `InvalidClaimsException.java`: This holds the custom Exception thrown when someone has an invalid JWT body, hasn't verified their email, or hasn't set up MFA. - - - - -### With the public key string - - - - -:::caution -This method is less secure compared to the first method because it disables key rotation of the access token signing key. -In this case, if the private key is somehow stolen, it can be used indefinitely to forge access tokens (Unless you manually change the key in the database). -::: - -Some JWT verification libraries require you to provide the JWT secret / public key for verification. -You can obtain the JWT secret from SuperTokens in the following way: - - - -## Query the `JWKS.json` endpoint: - -
- - ```bash - curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' - - { - "keys": [ - { - "kty": "RSA", - "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", - "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", - "e": "AQAB", - "alg": "RS256", - "use": "sig" - }, - { - "kty": "RSA", - "kid": "d-230...802340", - "n": "AMZruthvYz7...lx0rU8c=", - "e": "...", - "alg": "RS256", - "use": "sig" - } - ] - } - ``` - - :::important - The above shows an example output which returns two keys. - More keys could return based on the configured key rotation setting in the core. - If you notice, each key's `kid` starts with a `s-..` or a `d-..`. The `s-..` key is a static key that never changes, whereas `d-...` keys are dynamic keys that keep changing. If you are hard-coding public keys somewhere, you always want to pick the `s-..` key. - - One exception is that if you see a key with `kid` that doesn't start with `s-` or with `d-`, then treat that as a static key. - This only happens if you used to run an older SuperTokens core that was less than version `5.0`. - ::: - -## Run the NodeJS script below to convert the above output to a `PEM` file format. - -
- - ```tsx - import jwkToPem from 'jwk-to-pem'; - - // This JWK is copied from the result of the above SuperTokens core request - let jwk = { - "kty": "RSA", - "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", - "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", - "e": "AQAB", - "alg": "RS256", - "use": "sig" - }; - - // @ts-ignore - let certString = jwkToPem(jwk); - ``` - - The above snippet would generate the following certificate string: - - ```text - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I - QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm - ... (truncated for display) - XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT - xwIDAQAB - -----END PUBLIC KEY----- - ``` - - Use the generated Privacy-Enhanced Mail (PEM) string in your code as shown below: - - ```ts - import JsonWebToken from 'jsonwebtoken'; - - // Truncated for display - let certificate = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9IQQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm...XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStTxwIDAQAB\n-----END PUBLIC KEY-----"; - let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header - JsonWebToken.verify(jwt, certificate, function (err, decoded) { - let decodedJWT = decoded; - // Use JWT - }); - ``` - -## Tell SuperTokens to always only use the static key when creating a new session. - -You can accomplish this by setting the below configuration in the backend SDK: - - ```tsx - import SuperTokens from "supertokens-node"; - import Session from "supertokens-node/recipe/session"; - - SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-next-line - useDynamicAccessTokenSigningKey: false, - }) - ] - }); - ``` - -:::caution -Updating this value causes a spike in the session refresh API, as and when users visit your application. -::: - -
- -
- - -:::caution -Not applicable. Please use method 1 instead. -::: - - - - -:::caution -Not applicable. Please use method 1 instead. -::: - - - - -:::caution -Not applicable. Please use method 1 instead. -::: - - -
- -### Check for custom claim values for authorization - - - - -Once you have verified the access token, you can fetch the payload and perform authorization checks based on the values of the custom claims. For example, if you want to check if the user's email is verified, you should check the `st-ev` claim in the payload as shown below: - -```ts -import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); - -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); -} - -let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header -JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { - if (err) { - // send a 401 to the frontend.. - } - if (decoded !== undefined && typeof decoded !== "string") { - let isEmailVerified = (decoded as any)["st-ev"].v - if (!isEmailVerified) { - // send a 403 to the frontend.. - } - } -}); -``` - -Claims like email verification and user roles claims are included in the access token by the backend SDK automatically. You can even [add your own custom claims](/docs/additional-verification/session-verification/claim-validation#using-the-access-token-payload) to the access token payload, and those claims will be in the JWT as expected. - -:::important -On claim validation failure, you must send a `403` to the frontend, which causes the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. -::: - - - - -Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa), in `views.py`, between lines 20 and 28, the `st-ev` claim in the JWT payload undergoes verification. If the claim is not present or has a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. - - - - -Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d), in `main.go`, between lines 32 and 44, the `st-ev` claim in the JWT payload undergoes verification. If the claim is not present or has a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. - - - - -Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/5b2d19c02337ed7ee387723c84def9cd), in `JWTVerification.java`, between lines 42 and 58, the `st-ev` and `st-mfa` claims in the JWT payload undergo verification. -If the claims are not present or have a value of `false`, a `403` is sent to the frontend, causing the frontend SDK (pre-built UI SDK) to recheck the claims added on the frontend and navigate to the right screen. - - - - -### Check for anti-csrf during authorization - -:::important -You need to check for anti-cross-site request forgery (CSRF) for **non** GET requests when cookie-based authentication is active. +## Manual JWT verification + +Use a released SuperTokens `verifySession`, `getSession`, or equivalent API whenever one is available. These APIs validate +more than the JWT signature and expiry. For example, when you already have an access-token string rather than framework +request and response objects, use the released `getSessionWithoutRequestResponse` API shown above. + +Manual verification is only appropriate when your platform has no released SuperTokens backend SDK, or when an API gateway +cannot call one. Do not use mutable code snippets without pinned revisions as the verifier for a production system. Pin and +test the JWT library and verifier implementation you maintain. + +A manual verifier must reject the token unless **all** of these checks pass: + +1. Select the key by `kid` from `/auth/jwt/jwks.json`, and restrict verification to the expected signing + algorithm (`RS256`). Do not derive the accepted algorithm from the token-controlled header. +2. Verify the signature and expiry (`exp`). +3. Validate the token type using released SuperTokens semantics: if `stt` is present, require the numeric value `0`. + Released Node.js SDK 24.0.3 also accepts a missing `stt` for backward compatibility. Only accept that absence while + also validating a supported SuperTokens token header/version and that version's complete required payload shape. Reject + every other `stt` value or type. A valid JWT signed by a key in the JWKS is not necessarily a session access token. +4. Validate the required session payload shape and field types. Current access tokens require string values for `sub`, + `sessionHandle`, `refreshTokenHash1`, `rsub`, and `tId`, plus numeric `iat` and `exp`. Treat a changed or unknown token + shape as invalid rather than accepting it partially. +5. Validate every application authorization claim required by the route, such as tenant, role, permission, email + verification, or MFA state. Signature verification authenticates claims; it does not authorize the request. +6. For unsafe requests authenticated by cookies, perform the configured anti-CSRF check. With `VIA_CUSTOM_HEADER`, require + the `rid: session` header. With `VIA_TOKEN`, compare the `anti-csrf` request header with `antiCsrfToken` in the payload. + +The following access-token fields are managed by SuperTokens and must not be treated as application-defined fields: +`sub`, `iat`, `exp`, `sessionHandle`, `parentRefreshTokenHash1`, `refreshTokenHash1`, `antiCsrfToken`, `rsub`, `tId`, and +`stt`. + +:::warning[Manual verification has important limitations] +A signature-only verifier does not check session revocation or run the backend SDK's global claim validators. If immediate +revocation matters, ask the Core for the authoritative session state using the +[Get Session Information](/references/cdi/session-recipe/getsessioninfo) route with the session handle, or the +[Verify Session](/references/cdi/session-recipe/verifysession) route with the access token and `checkDatabase` set to +`true`. These database-backed checks confirm whether the session still exists and has not been revoked; the Verify Session +route defaults `checkDatabase` to `false`, and local JWT verification alone cannot determine revocation. Claim refresh +behavior is claim-specific: each claim defines its own refresh policy, and some built-in claims have no default maximum age. +Do not assume that all claims refresh on a fixed five-minute interval. ::: -Two methods exist for configuring [cross-site request forgery (CSRF) protection](/docs/post-authentication/session-management/security#anti-csrf): `VIA_CUSTOM_HEADER` and `VIA_TOKEN`. - -#### `VIA_CUSTOM_HEADER` - -`VIA_CUSTOM_HEADER` is automatically set if `sameSite` is `none` or if your `apiDomain` and `websiteDomain` do not share the same top level domain. -In this case, you need to check for the presence of the `rid` header from incoming requests. - -#### `VIA_TOKEN` - -When configured with `VIA_TOKEN`, an explicit `anti-csrf` token attaches as a header to requests with `anti-csrf` as the key. -To verify the `anti-csrf` token, you need to compare it to the value of the `antiCsrfToken` key from the payload of the decoded JWT. - --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/session-verification/protect-frontend-routes.mdx b/docs/additional-verification/session-verification/protect-frontend-routes.mdx index 716f484e61..17e3c3b41b 100644 --- a/docs/additional-verification/session-verification/protect-frontend-routes.mdx +++ b/docs/additional-verification/session-verification/protect-frontend-routes.mdx @@ -1,152 +1,148 @@ --- title: Protect frontend routes -hide_title: true -sidebar_position: 2 -description: >- - Protect frontend routes by requiring user sessions and verifying session - claims for access control. -page_type: guide -recipe: session -category: session-verification +description: Protect frontend routes by requiring user sessions and verifying session claims for access control. +sidebar: + order: 2 --- -# Protect frontend routes - - Protect frontend routes by requiring user sessions and verifying session claims for access control. +:::caution[Frontend guards are for user experience only] +Users can bypass client-side route guards or modify client-side state. Protect every API used by these pages with +backend session verification and the required role, permission, MFA, or verification claim validators. A frontend check +may control rendering or navigation, but it is not an authorization boundary. +::: ## Before you start - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: ---
- + - + ## Protect a route - - - + + You can wrap your components with the `` react component. This ensures that your component renders only if the user has logged in. If they are not logged in, the user gets redirected to the login page. + + +You can use the `doesSessionExist` function to check if a session exists in all your routes. + + -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import React from "react"; -import { - BrowserRouter, - Routes, - Route, -} from "react-router-dom"; -// highlight-next-line +import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; -// @ts-ignore import MyDashboardComponent from "./dashboard"; class App extends React.Component { - render() { - return ( - - - - - {/*Components that require to be protected by authentication*/} - - - // highlight-end - } /> - - - - ); - } + render() { + return ( + + + + + {/*Components that require to be protected by authentication*/} + + + } + /> + + + + ); + } +} +``` + + +```tsx +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } } ``` + + + + ### Optional session requirement You can provide the `requireAuth={false}` prop when using `` as shown below: + + -```tsx + + +```tsx import React from "react"; -import { - BrowserRouter, - Routes, - Route, -} from "react-router-dom"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; import { SuperTokensWrapper } from "supertokens-auth-react"; import Session, { SessionAuth } from "supertokens-auth-react/recipe/session"; class App extends React.Component { - render() { - return ( - - - - - // highlight-end - - - } /> - - - - ); - } + render() { + return ( + + + + + + + } + /> + + + + ); + } } -// highlight-start function MyDashboardComponent(props: any) { - let sessionContext = Session.useSessionContext(); - - if (sessionContext.loading) { - return null; - } - - if (sessionContext.doesSessionExist) { - // TODO: - } else { - // TODO: - } + let sessionContext = Session.useSessionContext(); + if (sessionContext.loading) { return null; -} -// highlight-end -``` + } - - - - -You can use the `doesSessionExist` function to check if a session exists in all your routes. + if (sessionContext.doesSessionExist) { + // TODO: + } else { + // TODO: + } -```tsx -import Session from 'supertokens-web-js/recipe/session'; - -async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } + return null; } ``` - - - - + + ## Check the claims of a session @@ -156,29 +152,67 @@ For example, you may want to check that the session has the admin role claim for You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: - - - + + ```tsx import React from "react"; -import { SessionAuth } from 'supertokens-auth-react/recipe/session'; -import { AccessDeniedScreen } from 'supertokens-auth-react/recipe/session/prebuiltui'; -import { UserRoleClaim, /*PermissionClaim*/ } from 'supertokens-auth-react/recipe/userroles'; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { - return ( - [ - ...globalValidators, UserRoleClaim.validators.includes("admin"), - ] - }> - {props.children} - - ); -} + return ( + [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + ]} + > + {props.children} + + ); +}; +``` + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + /* PermissionClaim.validators.includes("modify") */ + ], + }); + + if (validationErrors.length === 0) { + // user is an admin + return true; + } + + for (const err of validationErrors) { + if (err.id === UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } + } + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; +} ``` + + + + + Above, you create a generic component called `AdminRoute`, which enforces that its child components render only if the user has the admin role. In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component, which checks if the validators pass or not. @@ -186,171 +220,117 @@ If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass your own custom component to the `accessDeniedScreen` prop. -:::note -You can extend the `AdminRoute` component to check for other types of validators as well. +:::note[You can extend the `AdminRoute` component to check for other types of validators as well.] You can then reuse this component to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). ::: +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + +- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. +- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). +- We can also add a `PermissionClaim` validator to enforce a permission. If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + + + ```tsx import Session from "supertokens-auth-react/recipe/session"; -import {UserRoleClaim} from "supertokens-auth-react/recipe/userroles" +import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; function ProtectedComponent() { - let claimValue = Session.useClaimValue(UserRoleClaim) - if (claimValue.loading || !claimValue.doesSessionExist) { - return null; - } - let roles = claimValue.value; - if (Array.isArray(roles) && roles.includes("admin")) { - // User is an admin - } else { - // User doesn't have any roles, or is not an admin.. - } -} -``` - - - - - -```tsx -import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim, /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; - -async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - - // highlight-start - let validationErrors = await Session.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - UserRoleClaim.validators.includes("admin"), - /* PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end - - if (validationErrors.length === 0) { - // user is an admin - return true; - } - - for (const err of validationErrors) { - if (err.id === UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } - } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false + let claimValue = Session.useClaimValue(UserRoleClaim); + if (claimValue.loading || !claimValue.doesSessionExist) { + return null; + } + let roles = claimValue.value; + if (Array.isArray(roles) && roles.includes("admin")) { + // User is an admin + } else { + // User doesn't have any roles, or is not an admin.. + } } ``` - -- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. -- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). -- We can also add a `PermissionClaim` validator to enforce a permission. - - - -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - + + ```tsx import Session from "supertokens-web-js/recipe/session"; import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let roles = await Session.getClaimValue({claim: UserRoleClaim}); - if (Array.isArray(roles) && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end + if (await Session.doesSessionExist()) { + let roles = await Session.getClaimValue({ claim: UserRoleClaim }); + if (Array.isArray(roles) && roles.includes("admin")) { + // User is an admin + return true; } - // either a session does not exist, or the user is not an admin - return false + } + // either a session does not exist, or the user is not an admin + return false; } ``` + + - + - + - - - ## Protect a route You can use the `doesSessionExist` function to check if a session exists in all your routes. - - - - - + + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function doesSessionExist() { - if (await supertokensSession.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } + if (await supertokensSession.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } } ``` - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function doesSessionExist() { - if (await SuperTokens.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } + if (await SuperTokens.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -358,20 +338,22 @@ import org.json.JSONObject class MainApplication: Application() { fun doesSessionExist() { - val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); - if(accessTokenPayload != null) { - // user is logged in - } else { + if (!SuperTokens.doesSessionExist(this)) { // user has not logged in yet + return + } + + try { + SuperTokens.getAccessTokenPayloadSecurely(this) + // user is logged in + } catch (error: java.io.IOException) { + // the session expired, refresh failed, or the payload could not be read } } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -386,33 +368,31 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; Future doesSessionExist() async { - var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); - if (accessTokenPayload != null) { - // user is logged in - } else { - // user has not logged in yet - } + if (!await SuperTokens.doesSessionExist()) { + // user has not logged in yet + return; + } + + try { + await SuperTokens.getAccessTokenPayloadSecurely(); + // user is logged in + } catch (error) { + // the session expired, refresh failed, or the payload could not be read + } } ``` + + + + - - - - - - - --- @@ -425,194 +405,125 @@ For example, you may want to check that the session has the admin role claim for You can achieve this using the session claims validator feature. Let's take an example of using the user roles claim to check if the session has the admin claim: - - - - - + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim, /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - - // highlight-start - let validationErrors = await Session.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - UserRoleClaim.validators.includes("admin"), - /* PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end - - if (validationErrors.length === 0) { - // user is an admin - return true; - } - - for (const err of validationErrors) { - if (err.id === UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + /* PermissionClaim.validators.includes("modify") */ + ], + }); + + if (validationErrors.length === 0) { + // user is an admin + return true; } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false -} -``` - -- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. -- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. -This may include a validator that enforces that you have verified the user's email (if enabled by you). -- We can also add a `PermissionClaim` validator to enforce a permission. - - - -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: -```tsx -import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; - -async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let roles = await Session.getClaimValue({claim: UserRoleClaim}); - if (roles !== undefined && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end + for (const err of validationErrors) { + if (err.id === UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } } - // either a session does not exist, or the user is not an admin - return false + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensUserRoles from "supertokens-web-js-script/recipe/userroles"; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - - // highlight-start - let validationErrors = await supertokensSession.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - supertokensUserRoles.UserRoleClaim.validators.includes("admin"), - /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end - - if (validationErrors.length === 0) { - // user is an admin - return true; - } - - for (const err of validationErrors) { - if (err.id === supertokensUserRoles.UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + supertokensUserRoles.UserRoleClaim.validators.includes("admin"), + /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ + ], + }); + + if (validationErrors.length === 0) { + // user is an admin + return true; } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false -} -``` - -- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. -- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). -- We can also add a `PermissionClaim` validator to enforce a permission. - -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensUserRoles from "supertokens-web-js-script/recipe/userroles"; -async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - // highlight-start - let roles = await supertokensSession.getClaimValue({claim: supertokensUserRoles.UserRoleClaim}); - if (roles !== undefined && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end + for (const err of validationErrors) { + if (err.id === supertokensUserRoles.UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } } - // either a session does not exist, or the user is not an admin - return false + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; } ``` - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function getRole() { - if (await SuperTokens.doesSessionExist()) { + if (await SuperTokens.doesSessionExist()) { + let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; - // highlight-start - let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; - - if (roles.includes("admin")) { - // TODO.. - } else { - // TODO.. - } - // highlight-end + if (roles.includes("admin")) { + // TODO.. + } else { + // TODO.. } + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens +import org.json.JSONArray import org.json.JSONObject class MainApplication: Application() { fun checkIfUserIsAnAdmin() { - val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this); - val roles: List = (accessTokenPayload.get("st-role") as JSONObject).get("v") as List; - if (roles.contains("admin")) { - // user is an admin - } else { - // user is not an admin + if (!SuperTokens.doesSessionExist(this)) return + + try { + val accessTokenPayload: JSONObject = SuperTokens.getAccessTokenPayloadSecurely(this) + val rolesJson: JSONArray = accessTokenPayload.getJSONObject("st-role").getJSONArray("v") + val roles = (0 until rolesJson.length()).map { rolesJson.getString(it) } + + if (roles.contains("admin")) { + // user is an admin + } else { + // user is not an admin + } + } catch (error: java.io.IOException) { + // the session expired, refresh failed, or the payload could not be read } } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -629,22 +540,21 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; Future checkIfUserIsAnAdmin() async { - var accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); + if (!await SuperTokens.doesSessionExist()) return; + try { + final accessTokenPayload = await SuperTokens.getAccessTokenPayloadSecurely(); if (accessTokenPayload.containsKey("st-role")) { - Map roleObject = accessTokenPayload["st-role"]; + final roleObject = accessTokenPayload["st-role"] as Map; if (roleObject.containsKey("v")) { - List roles = roleObject["v"]; + final roles = (roleObject["v"] as List).whereType().toList(); if (roles.contains("admin")) { // user is an admin @@ -653,30 +563,93 @@ Future checkIfUserIsAnAdmin() async { } } } + } catch (error) { + // the session expired, refresh failed, or the payload could not be read + } } ``` + + + + + + + + + +- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. +- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. +This may include a validator that enforces that you have verified the user's email (if enabled by you). +- We can also add a `PermissionClaim` validator to enforce a permission. + +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + +- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. +- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that you have verified the user's email (if enabled by you). +- We can also add a `PermissionClaim` validator to enforce a permission. - +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + + + + + + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; - +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let roles = await Session.getClaimValue({ claim: UserRoleClaim }); + if (roles !== undefined && roles.includes("admin")) { + // User is an admin + return true; + } + } + // either a session does not exist, or the user is not an admin + return false; +} +``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +async function shouldLoadRoute(): Promise { + if (await supertokensSession.doesSessionExist()) { + let roles = await supertokensSession.getClaimValue({ claim: supertokensUserRoles.UserRoleClaim }); + if (roles !== undefined && roles.includes("admin")) { + // User is an admin + return true; + } + } + // either a session does not exist, or the user is not an admin + return false; +} +``` + + + + - - - + -:::tip Feature -You can also [build your own custom claim validators](/docs/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. +:::tip[Feature] +You can also [build your own custom claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) based on your app's requirements. ::: --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/session-verification/ssr.mdx b/docs/additional-verification/session-verification/ssr.mdx index 871116e09a..f07900c47d 100644 --- a/docs/additional-verification/session-verification/ssr.mdx +++ b/docs/additional-verification/session-verification/ssr.mdx @@ -1,21 +1,10 @@ --- -id: ssr -title: Session verification during server side rendering -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 3 -description: >- - Enable session verification during server-side rendering using cookie-based - sessions and JWT verification. -page_type: guide -recipe: session -category: session-verification +title: Session verification during server-side rendering +description: Enable secure session verification during server-side rendering with cookie-based sessions. +sidebar: + order: 3 --- - - -# Session verification during server side rendering - ## Overview In Server Side Rendering (SSR) scenarios, the session verification process is slightly different. @@ -23,24 +12,39 @@ Check the following guide to understand how to adjust the flow to work with SSR. ## Before you start -Getting access to the session during server side rendering is only possible using cookie-based sessions. -This is the default setting, but you have to keep this in mind if you want to switch to header-based sessions. +For an ordinary browser navigation, the browser can send cookie-based session tokens to the server performing SSR. It +cannot add SuperTokens' header-transfer token to that navigation. Cookie transfer is the default; the cookie's domain, +path, `SameSite`, and `Secure` attributes must allow it to reach the SSR server. - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: ## Steps -### 1. Enable sharing of cookies across sub domains +### 1. Enable sharing of cookies across subdomains -If your API layer and website are on different sub domains (like `example.com` and `api.example.com`), then by default, the session tokens attach only to `api.example.com`. +If your API layer and website are on different subdomains (like `example.com` and `api.example.com`), then by default, the session tokens attach only to `api.example.com`. Change this to ensure that the session tokens attach to `.example.com` and the access token cookie goes to your web server on `example.com`. -Enable this by [setting the `cookieDomain` configuration on the backend](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). +Enable this by [setting the `cookieDomain` configuration on the backend](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). + +### 2. Verify the session + +Prefer the released session verification API for your backend framework. For example, the Node.js Next.js integration +exports `getSSRSession`, which accepts the request's cookies and validates the access-token signature, expiry, and payload +shape. It does not run global claim validators or perform an authoritative database check for session revocation. Treat its +payload as authenticated claims, not as a complete authorization decision: explicitly validate every claim required by the +page, such as tenant, role, permission, email verification, or MFA state. -### 2. Do JWT verification +Before rendering sensitive protected data for which immediate revocation matters, call an authoritative backend/API +endpoint that verifies the session with database checking enabled. Other backend frameworks can use `getSession` or +`verifySession`, provided the SSR response propagates any token updates that the session API attaches. If your platform has +no released SuperTokens SDK, follow the strict requirements in the +[manual verification fallback](/additional-verification/session-verification/protect-api-routes#manual-jwt-verification), +not signature-only JWT verification. -Follow the [JWT verification guide](/docs/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library) to verify the JWT on the server side. -You can read the JWT from the `sAccessToken` cookie in the request object. -If the JWT is missing, invalid, or expired, redirect the user to a `/refresh-session?redirectBack=` page. You can have any path for this since it's your website. +If the access token is missing, invalid, or expired, redirect the user to a +`/refresh-session?redirectBack=` page. You can use another local path on your website instead. On the `/refresh-session` page, you want to call the `attemptRefreshingSession` function (from the client side). This function attempts to refresh the session. @@ -49,148 +53,156 @@ If it fails, it returns `false`. If it returns `true`, you want to redirect the user back to the page they were on. If it returns `false`, you want to redirect the user to the login page. -### 3. Implement the refresh session flow (`/refresh-session` page) +### 3. Implement the refresh-session flow (`/refresh-session` page) -On this path, attempt refreshing the session which can yield: -- Success: The frontend gets new access and refresh tokens. -Post this, redirect the user to the path mentioned on the `redirectBack` query parameter. -- Failure: This would happen if the session has expired or the backend has revoked it. -Either way, you want to redirect the user to the login page. +On this path, attempt to refresh the session. This can produce either result: -Below is the code snippet that you can use on the `/refresh-session` path on the frontend +- **Success:** The frontend gets new access and refresh tokens. Redirect the user to the validated local path in the + `redirectBack` query parameter. +- **Failure:** The session has expired or the backend has revoked it. Redirect the user to the login page. - +Below is the code snippet that you can use on the `/refresh-session` path on the frontend - + - - + + + ```tsx import React from "react"; -import Session from "supertokens-auth-react/recipe/session" +import Session from "supertokens-auth-react/recipe/session"; import SuperTokens from "supertokens-auth-react"; -export function AttemptRefresh(props: any) { - React.useEffect(() => { - let cancel = false; - Session.attemptRefreshingSession().then(success => { - if (cancel) { - // component has unmounted somehow.. - return; - } - if (success) { - // we have new session tokens, so we redirect the user back - // to where they were. - const urlParams = new URLSearchParams(window.location.search); - window.location.href = urlParams.get('redirectBack')!; - } else { - // we redirect to the login page since the user - // is now logged out - SuperTokens.redirectToAuth(); - } - }) - return () => { - cancel = true; - } - }, []); - return null; +export function AttemptRefresh() { + React.useEffect(() => { + let cancel = false; + Session.attemptRefreshingSession().then((success) => { + if (cancel) { + // component has unmounted somehow.. + return; + } + if (success) { + // we have new session tokens, so we redirect the user back + // to where they were. + const urlParams = new URLSearchParams(window.location.search); + const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); + window.location.href = + redirectBack.origin === window.location.origin + ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` + : "/"; + } else { + // we redirect to the login page since the user + // is now logged out + SuperTokens.redirectToAuth(); + } + }); + return () => { + cancel = true; + }; + }, []); + return null; } ``` - - - - - + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; function attemptRefresh() { - Session.attemptRefreshingSession().then(success => { - if (success) { - // we have new session tokens, so we redirect the user back - // to where they were. - const urlParams = new URLSearchParams(window.location.search); - window.location.href = urlParams.get('redirectBack')!; - } else { - // we redirect to the login page since the user - // is now logged out - window.location.href = "/login" - } - }) + Session.attemptRefreshingSession().then((success) => { + if (success) { + // we have new session tokens, so we redirect the user back + // to where they were. + const urlParams = new URLSearchParams(window.location.search); + const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); + window.location.href = + redirectBack.origin === window.location.origin + ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` + : "/"; + } else { + // we redirect to the login page since the user + // is now logged out + window.location.href = "/login"; + } + }); } ``` + + - + - + - - - - - - - + + +:::warning[Server side rendering is not applicable for mobile apps.] +::: + + + + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; function attemptRefresh() { - Session.attemptRefreshingSession().then(success => { - if (success) { - // we have new session tokens, so we redirect the user back - // to where they were. - const urlParams = new URLSearchParams(window.location.search); - window.location.href = urlParams.get('redirectBack')!; - } else { - // we redirect to the login page since the user - // is now logged out - window.location.href = "/login" - } - }) + Session.attemptRefreshingSession().then((success) => { + if (success) { + // we have new session tokens, so we redirect the user back + // to where they were. + const urlParams = new URLSearchParams(window.location.search); + const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); + window.location.href = + redirectBack.origin === window.location.origin + ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` + : "/"; + } else { + // we redirect to the login page since the user + // is now logged out + window.location.href = "/login"; + } + }); } ``` - - - - -```tsx -import supertokensSession from 'supertokens-web-js-script/recipe/session'; + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" function attemptRefresh() { - supertokensSession.attemptRefreshingSession().then(success => { - if (success) { - // we have new session tokens, so we redirect the user back - // to where they were. - const urlParams = new URLSearchParams(window.location.search); - window.location.href = urlParams.get('redirectBack')!; - } else { - // we redirect to the login page since the user - // is now logged out - window.location.href = "/login" - } - }) + supertokensSession.attemptRefreshingSession().then((success) => { + if (success) { + // we have new session tokens, so we redirect the user back + // to where they were. + const urlParams = new URLSearchParams(window.location.search); + const redirectBack = new URL(urlParams.get("redirectBack") ?? "/", window.location.origin); + window.location.href = + redirectBack.origin === window.location.origin + ? `${redirectBack.pathname}${redirectBack.search}${redirectBack.hash}` + : "/"; + } else { + // we redirect to the login page since the user + // is now logged out + window.location.href = "/login"; + } + }); } ``` + + + + - - + + - - -:::caution -Server side rendering is not applicable for mobile apps. -::: - - - - - - + #### Why trigger the refresh session flow instead of redirecting the user to the login page directly? @@ -202,23 +214,26 @@ If the user goes to the login page directly, then in the second case, the fronte To counteract this issue, redirect the user to a refresh page, which creates a new access token. In the first case, when the user is actually logged out, the refreshing fails, and they go to the login page anyway. -#### Can `verifySession` or `getSession` replace manual JWT verification? +#### Can `verifySession` or `getSession` be used during SSR? + +Yes. A released, read-only SSR helper such as Node.js Next.js `getSSRSession` can authenticate the access token, but the +application must still validate the authorization claims required by the page. The helper does not run global claim +validators or authoritatively check database revocation. For sensitive data that requires immediate revocation, verify the +session through an authoritative backend/API endpoint with database checking before rendering. -Yes, you can, but it is not recommended because: -- Often the web server is on a different process than the API server. -Using the backend SDK requires you to give it the credentials to the SuperTokens core, which you might not want to provide to the web server. -- The `session` object resulting from `verifySession` and `getSession` makes it easy to mutate the access token payload. -These mutations reflect on the frontend via network interceptors from the frontend SDK. -In SSR, the browser makes the call to your web server directly, and the frontend SDK interceptors don't run. -This can cause inconsistency between the access token payload as seen on the frontend vs the backend. +You can also use `verifySession` or `getSession`, but those APIs may attach rotated or updated tokens to the response. The +SSR server must propagate those updates because frontend SDK network interceptors do not run for the browser's navigation +request. If the SSR process should not receive SuperTokens Core credentials, use the read-only helper for authentication +and an authoritative backend/API check for sensitive authorization, or use a manual verifier that meets every requirement +in the linked fallback guide. --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/session-verification/with-websocket.mdx b/docs/additional-verification/session-verification/with-websocket.mdx index 9da03a2d10..d60d51b8fb 100644 --- a/docs/additional-verification/session-verification/with-websocket.mdx +++ b/docs/additional-verification/session-verification/with-websocket.mdx @@ -1,63 +1,54 @@ --- -id: with-websocket title: WebSocket session verification -hide_title: true -sidebar_position: 4 -description: >- - Verify WebSocket sessions by fetching and sending JWTs during socket - connection initialization. -page_type: guide -recipe: session -category: session-verification +description: Authenticate WebSocket connections with SuperTokens access tokens and enforce connection-lifetime checks. +sidebar: + order: 4 --- - -# WebSocket Session Verification - ## Overview -Socket connections do not always use HTTP, which means you cannot use cookies or HTTP authorization header here. -Instead, the frontend must fetch the JWT and send it at the start of the socket connection. +WebSocket connections begin with an HTTP upgrade request, and Socket.IO may begin with HTTP long-polling. Eligible cookies +can accompany these requests. Browser WebSocket clients cannot set arbitrary headers, although non-browser clients can. +This guide passes an access token in Socket.IO's handshake `auth` payload when cookie authentication is not suitable. ## Before you start - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: ## Steps ### 1. Expose the JWT to the frontend Ensure that the JWT is available to the frontend. -This is already the case in header based auth, but if you are using cookie based auth, then you should set the following boolean to true in `session.init` on the backend: - - - +This is already the case in header-based authentication. If you use cookie-based authentication, set the following boolean +to `true` in `session.init` on the backend: + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - exposeAccessTokenToFrontendInCookieBasedAuth: true - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + exposeAccessTokenToFrontendInCookieBasedAuth: true, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -69,127 +60,120 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ExposeAccessTokenToFrontendInCookieBasedAuth: true, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-next-line expose_access_token_to_frontend_in_cookie_based_auth=True ) ] ) ``` + + - - - -### 2. Send the JWT on socket connection start +### 2. Send the access token when connecting -On the client side, when you create a socket connection, you must fetch the JWT from the session and use that as follows.: +Fetch the access token before creating the socket connection. Send it in Socket.IO's `auth` payload, not the query string; +query-string tokens are commonly retained in URLs, proxy logs, and monitoring systems. Always use `https`/`wss` in +production and enforce an approved origin list on the server. -```tsx -import Session from "supertokens-web-js/recipe/session" -declare let io: any; // REMOVE_FROM_OUTPUT +```tsx check=false reason="Socket.IO server instance is created in the surrounding framework setup" +import Session from "supertokens-web-js/recipe/session"; async function initSocketConnection() { - const token = await Session.getAccessToken(); - if (token === undefined) { - throw new Error("User is not logged in"); - } - const socket = io.connect('http://localhost:3000', { - query: { token } - }); - return socket; + const token = await Session.getAccessToken(); + if (token === undefined) { + throw new Error("User is not logged in"); + } + const socket = io.connect("https://api.example.com", { + auth: { token }, + }); + return socket; } ``` -- See the [docs on how to fetch the access token on the frontend for all frameworks](/docs/post-authentication/session-management/access-session-data#on-the-frontend) if needed. - The `Session.getAccessToken()` function auto refreshes the session before returning the JWT if needed. -:::caution -Make sure to close the socket connection whenever appropriate to avoid sending stale JWTs. -::: - -### 3. Verify the JWT +### 3. Verify the session -Verify the JWT on socket connection initialisation on the backend: +Use a released backend session API rather than a signature-only JWT verifier. The Node.js example below validates the +complete SuperTokens access-token structure, expiry, session claims, and revocation state before accepting the connection. - -```tsx -import jwt, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; -declare let io: any // REMOVE_FROM_OUTPUT - -// functions to fetch jwks -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); +```tsx check=false reason="Socket.IO server instance and application authorization validators are defined by the application" +import Session from "supertokens-node/recipe/session"; -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); -} +io.use(async (socket, next) => { + try { + const token = socket.handshake.auth.token; + if (typeof token !== "string") { + throw new Error("Missing access token"); + } -// socket io connection -io.use(function (socket: any, next: any) { - // highlight-start - // we first try and verify the jwt from the token param. - if (socket.handshake.query && socket.handshake.query.token) { - jwt.verify(socket.handshake.query.token, getKey, {}, function (err, decoded) { - if (err) return next(new Error('Authentication error')); - socket.decoded = decoded; - next(); + const session = await Session.getSessionWithoutRequestResponse(token, undefined, { + antiCsrfCheck: false, + checkDatabase: true, }); - } - else { - next(new Error('Authentication error')); - } - // highlight-end -}) - .on('connection', function (socket: any) { - // Connection now authenticated to receive further events - socket.on('message', function (message: string) { - io.emit('message', message); - }); + socket.data.accessToken = token; + socket.data.session = session; + next(); + } catch { + next(new Error("Authentication error")); + } +}).on("connection", (socket) => { + const payload = socket.data.session.getAccessTokenPayload(); + const expiresInMs = Math.max(0, payload.exp * 1000 - Date.now()); + const expiryTimer = setTimeout(() => socket.disconnect(true), expiresInMs); + + socket.on("message", async (message: string, acknowledge?: (error?: string) => void) => { + try { + // Recheck revocation and configured authorization claims before privileged events. + await Session.getSessionWithoutRequestResponse(socket.data.accessToken, undefined, { + antiCsrfCheck: false, + checkDatabase: true, + }); + io.emit("message", message); + acknowledge?.(); + } catch { + acknowledge?.("Authentication error"); + socket.disconnect(true); + } }); -``` -:::important -Post verification, ensure that the claims in the JWT align with your authorization rules. - -For example, if your app requires that the user verifies their email before they use it, check that the `decoded["st-ev"].v` claim in the JWT equals `true`. + socket.on("disconnect", () => clearTimeout(expiryTimer)); +}); +``` -Normally, the backend SDK's `getSession` or `verifySession` function performs this check for you based on your configuration. However, since JWT verification is happening manually here, you need to do those checks yourself. +:::warning[Define a connection-lifetime policy] +Authentication at connection time is not enough: a connection can outlive token expiry, session revocation, or an +authorization change. Disconnect no later than the access token's `exp`, and revalidate before privileged events or on a +short application-defined interval. Use database checking when immediate session revocation matters. After disconnecting, +the client must refresh its session and reconnect with a new access token. ::: --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/user-roles/_blocks/assign-role-to-session.mdx b/docs/additional-verification/user-roles/_blocks/assign-role-to-session.mdx deleted file mode 100644 index c86a224ae8..0000000000 --- a/docs/additional-verification/user-roles/_blocks/assign-role-to-session.mdx +++ /dev/null @@ -1,82 +0,0 @@ - - - - -```tsx -import {UserRoleClaim, PermissionClaim} from "supertokens-node/recipe/userroles"; -import {SessionContainer} from "supertokens-node/recipe/session" - -async function addRolesAndPermissionsToSession(session: SessionContainer) { - // we add the user's roles to the user's session - await session.fetchAndSetClaim(UserRoleClaim) - - // we add the permissions of a user to the user's session - await session.fetchAndSetClaim(PermissionClaim) -} -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" -) - -func addRolesAndPermissionsToSession(session sessmodels.SessionContainer) error { - // we add the user's roles to the user's session - err := session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) - if err != nil { - return err - } - - // we add the user's permissions to the user's session - err = session.FetchAndSetClaim(userrolesclaims.PermissionClaim) - if err != nil { - return err - } - - return nil -} -``` - - - - - - - -```python -from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim - - -async def add_roles_and_permissions_to_session(session: SessionContainer): - # we add the user's roles to the user's session - await session.fetch_and_set_claim(UserRoleClaim) - - # we add the user's permissions to the user's session - await session.fetch_and_set_claim(PermissionClaim) -``` - - - - -```python -from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim - - -def add_roles_and_permissions_to_session(session: SessionContainer): - # we add the user's roles to the user's session - session.sync_fetch_and_set_claim(UserRoleClaim) - - # we add the user's permissions to the user's session - session.sync_fetch_and_set_claim(PermissionClaim) -``` - - - - - diff --git a/docs/additional-verification/user-roles/_blocks/assign-role-to-user.mdx b/docs/additional-verification/user-roles/_blocks/assign-role-to-user.mdx deleted file mode 100644 index 591f26eba3..0000000000 --- a/docs/additional-verification/user-roles/_blocks/assign-role-to-user.mdx +++ /dev/null @@ -1,119 +0,0 @@ - - - - - -```tsx -import UserRoles from "supertokens-node/recipe/userroles"; - -async function addRoleToUser(userId: string) { - // highlight-start - const response = await UserRoles.addRoleToUser("public", userId, "user"); - - if (response.status === "UNKNOWN_ROLE_ERROR") { - // No such role exists - return; - } - - if (response.didUserAlreadyHaveRole === true) { - // The user already had the role - } - // highlight-end -} -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/userroles" -) - -func addRoleToUser(userId string) { - // highlight-start - response, err := userroles.AddRoleToUser("public", userId, "user", nil) - if err != nil { - // TODO: Handle error - return - } - - if response.UnknownRoleError != nil { - // No such role exists - return - } - - if response.OK.DidUserAlreadyHaveRole { - // The user already had the role - } - // highlight-end -} -``` - - - - - - - -```python -from supertokens_python.recipe.userroles.asyncio import add_role_to_user -from supertokens_python.recipe.userroles.interfaces import UnknownRoleError - - -async def add_role_to_user_func(user_id: str): - # highlight-start - role = "user" - res = await add_role_to_user("public", user_id, role) - if isinstance(res, UnknownRoleError): - # No such role exists - return - - if res.did_user_already_have_role: - # User already had this role - pass - # highlight-end -``` - - - - -```python -from supertokens_python.recipe.userroles.syncio import add_role_to_user -from supertokens_python.recipe.userroles.interfaces import UnknownRoleError - - -def add_role_to_user_func(user_id: str): - # highlight-start - role = "user" - res = add_role_to_user("public", user_id, role) - if isinstance(res, UnknownRoleError): - # No such role exists - return - - if res.did_user_already_have_role: - # User already had this role - pass - # highlight-end - -``` - - - - - - - -```bash -curl --location --request PUT 'http://localhost:3567/recipe/user/role' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "userId": "fa7a0841-b533-4478-95533-0fde890c3483", - "role": "user" -}' -``` - - - - diff --git a/docs/additional-verification/user-roles/_blocks/create-role.mdx b/docs/additional-verification/user-roles/_blocks/create-role.mdx deleted file mode 100644 index 14c498a206..0000000000 --- a/docs/additional-verification/user-roles/_blocks/create-role.mdx +++ /dev/null @@ -1,108 +0,0 @@ - - - - -```tsx -import UserRoles from "supertokens-node/recipe/userroles"; - -async function createRole() { - // highlight-start - const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); - - if (response.createdNewRole === false) { - // The role already exists - } - // highlight-end -} -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/userroles" -) - -func createRole() { - // highlight-start - resp, err := userroles.CreateNewRoleOrAddPermissions("user", []string{ - "read", - }, nil) - - if err != nil { - // TODO: Handle error - return - } - if resp.OK.CreatedNewRole == false { - // The role already exists - } - // highlight-end -} -``` - - - - - - - -```python -from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions - -async def create_role(): - # highlight-start - res = await create_new_role_or_add_permissions("user", ["read"]) - if not res.created_new_role: - # The role already existed - pass - # highlight-end - -``` - - - - - -```python -from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions - -def create_role(): - # highlight-start - res = create_new_role_or_add_permissions("user", ["read"]) - if not res.created_new_role: - # The role already existed - pass - # highlight-end - -``` - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/role' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "role": "user", - "permissions": [ - "read" - ] -}' - -``` - - - - - -Create Role - - - - diff --git a/docs/additional-verification/user-roles/_category_.json b/docs/additional-verification/user-roles/_category_.json deleted file mode 100644 index f02d0f2e64..0000000000 --- a/docs/additional-verification/user-roles/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "User Roles", - "position": 4 -} diff --git a/docs/additional-verification/user-roles/initial-setup.mdx b/docs/additional-verification/user-roles/initial-setup.mdx index 8bfccd55a0..6379d36f72 100644 --- a/docs/additional-verification/user-roles/initial-setup.mdx +++ b/docs/additional-verification/user-roles/initial-setup.mdx @@ -1,30 +1,30 @@ --- -sidebar_position: 2 title: Initial Setup -toc_max_heading_level: 4 -hide_title: true description: Learn how to initialize and configure user roles with SuperTokens. -page_type: tutorial -recipe: userroles -category: user-roles +sidebar: + order: 2 --- + +Add SuperTokens roles and permissions to this application. Inspect the existing backend, frontend, session configuration, and tenant model first. Define roles and permissions that match the application's resources, initialize the UserRoles recipe, assign roles to users, and protect backend and frontend routes. Check whether role data should be included in access tokens, preserve existing authorization conventions, and validate authorized, unauthorized, and cross-tenant access. + -import CreateRole from "./_blocks/create-role.mdx"; -import AssignRoleToUser from "./_blocks/assign-role-to-user.mdx"; -import AssignRoleToSession from "./_blocks/assign-role-to-session.mdx"; - -# Initial setup +## Overview +When you work with the `UserRoles` recipe you should follow these steps: + + + + -## Overview + + -When you work with the `UserRoles` recipe you should follow these steps: - -## Create a role and assign permissions to it -## Assign roles to users -## Protect frontend and backend routes by verifying that the user has the correct role and permissions + The next sections show you the actual instructions on how to achieve this. @@ -32,7 +32,7 @@ The next sections show you the actual instructions on how to achieve this. ## Before you start -:::info Multi Tenancy +:::info[Multi Tenancy] In a multi tenant setup, roles, and permissions share across all tenants, however, the mapping of users to roles are on a per tenant level. @@ -46,32 +46,26 @@ If you have user ID `user1` that has access to `tenant1` and `tenant2`, you can ### 1. Initialize the recipe - - - + + ```tsx import SuperTokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-next-line - UserRoles.init(), - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [UserRoles.init()], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" @@ -81,17 +75,14 @@ import ( func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ - // highlight-next-line userroles.Init(nil), }, }) } ``` - - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import userroles @@ -99,37 +90,30 @@ init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), - framework='...', # type: ignore + framework='...', recipe_list=[ # Initialize other recipes as seen in the quick setup guide - # highlight-next-line userroles.init() ] ) ``` - - - + + By default, the user roles recipe adds the roles and permission information into a user's session (if they have assigned roles & permissions). If you do not want roles or permissions information in the session, or want to manually add it yourself, you can provide the following input configs to the `UserRoles.init` function: - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; UserRoles.init({ - // highlight-start - skipAddingRolesToAccessToken: true, - skipAddingPermissionsToAccessToken: true, - // highlight-end -}) + skipAddingRolesToAccessToken: true, + skipAddingPermissionsToAccessToken: true, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" @@ -141,21 +125,17 @@ func main() { supertokens.Init(supertokens.TypeInput{ AppInfo: supertokens.AppInfo{ /*...*/ }, RecipeList: []supertokens.Recipe{ - // highlight-start userroles.Init(&userrolesmodels.TypeInput{ SkipAddingRolesToAccessToken: true, SkipAddingPermissionsToAccessToken: true, }), - // highlight-end }, }) } ``` - - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import userroles @@ -163,18 +143,15 @@ init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), - framework='...', # type: ignore + framework='...', recipe_list=[ - # highlight-start userroles.init(skip_adding_roles_to_access_token=True, skip_adding_permissions_to_access_token=True) - # highlight-end ] ) ``` - - - + + ### 2. Create roles and permissions @@ -184,7 +161,93 @@ They should represent entities and actions that are relevant to your business lo To create them use the next code snippet as a reference. When you create a role you can also include the permissions that the role should have. - + + +Create Role + + + + + +```tsx +import UserRoles from "supertokens-node/recipe/userroles"; + +async function createRole() { + const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); + + if (response.createdNewRole === false) { + // The role already exists + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/userroles" +) + +func createRole() { + resp, err := userroles.CreateNewRoleOrAddPermissions("user", []string{ + "read", + }, nil) + + if err != nil { + // TODO: Handle error + return + } + if resp.OK.CreatedNewRole == false { + // The role already exists + } +} +``` + + + + +```python +from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions + +async def create_role(): + res = await create_new_role_or_add_permissions("user", ["read"]) + if not res.created_new_role: + # The role already existed + pass + +``` + + +```python +from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions + +def create_role(): + res = create_new_role_or_add_permissions("user", ["read"]) + if not res.created_new_role: + # The role already existed + pass + +``` + + + + +```bash +curl --location --request PUT '/recipe/role' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "role": "user", + "permissions": [ + "read" + ] +}' + +``` + + + + + ### 3. Assign roles to users @@ -192,19 +255,185 @@ When you create a role you can also include the permissions that the role should After you create a user account, you can assign roles to them. You can do this by overriding the authentication recipes with a function that calls the `UserRoles` API after a successful sign up. The next code snippet shows you what function to call to connect a user to a role. -To figure out where to call that function, check the documentation for the authentication method that you use: [passwordless](/docs/authentication/passwordless/hooks-and-overrides), [email-password](/docs/authentication/email-password/hooks-and-overrides) or [third-party](/docs/authentication/social/hooks-and-overrides). +To figure out where to call that function, check the documentation for the authentication method that you use: [passwordless](/authentication/passwordless/hooks-and-overrides), [email-password](/authentication/email-password/hooks-and-overrides) or [third-party](/authentication/social/hooks-and-overrides). - + + +```tsx +import UserRoles from "supertokens-node/recipe/userroles"; + +async function addRoleToUser(userId: string) { + const response = await UserRoles.addRoleToUser("public", userId, "user"); + + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + return; + } + + if (response.didUserAlreadyHaveRole === true) { + // The user already had the role + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/userroles" +) + +func addRoleToUser(userId string) { + response, err := userroles.AddRoleToUser("public", userId, "user", nil) + if err != nil { + // TODO: Handle error + return + } + + if response.UnknownRoleError != nil { + // No such role exists + return + } + + if response.OK.DidUserAlreadyHaveRole { + // The user already had the role + } +} +``` + + + + +```python +from supertokens_python.recipe.userroles.asyncio import add_role_to_user +from supertokens_python.recipe.userroles.interfaces import UnknownRoleError + + +async def add_role_to_user_func(user_id: str): + role = "user" + res = await add_role_to_user("public", user_id, role) + if isinstance(res, UnknownRoleError): + # No such role exists + return + + if res.did_user_already_have_role: + # User already had this role + pass +``` + + +```python +from supertokens_python.recipe.userroles.syncio import add_role_to_user +from supertokens_python.recipe.userroles.interfaces import UnknownRoleError + + +def add_role_to_user_func(user_id: str): + role = "user" + res = add_role_to_user("public", user_id, role) + if isinstance(res, UnknownRoleError): + # No such role exists + return + + if res.did_user_already_have_role: + # User already had this role + pass + +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/user/role' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "userId": "fa7a0841-b533-4478-95533-0fde890c3483", + "role": "user" +}' +``` + + #### Assign roles to a session If you want to associate a role to a user after you create a session, you can do this by manually calling the function described in the next code snippet. -For information on how to access the session object that you need to pass to the function, check either the [`Verify Session`](/docs/additional-verification/session-verification/protect-api-routes#using-verify-session) or the [`Get Session`](/docs/additional-verification/session-verification/protect-api-routes#using-get-session) documentation. +For information on how to access the session object that you need to pass to the function, check either the [`Verify Session`](/additional-verification/session-verification/protect-api-routes#using-verify-session) or the [`Get Session`](/additional-verification/session-verification/protect-api-routes#using-get-session) documentation. - + + +```tsx +import { UserRoleClaim, PermissionClaim } from "supertokens-node/recipe/userroles"; +import { SessionContainer } from "supertokens-node/recipe/session"; + +async function addRolesAndPermissionsToSession(session: SessionContainer) { + // we add the user's roles to the user's session + await session.fetchAndSetClaim(UserRoleClaim); + + // we add the permissions of a user to the user's session + await session.fetchAndSetClaim(PermissionClaim); +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" + "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" +) + +func addRolesAndPermissionsToSession(session sessmodels.SessionContainer) error { + // we add the user's roles to the user's session + err := session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) + if err != nil { + return err + } -:::info Multi Tenancy + // we add the user's permissions to the user's session + err = session.FetchAndSetClaim(userrolesclaims.PermissionClaim) + if err != nil { + return err + } + + return nil +} +``` + + + + +```python +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim + + +async def add_roles_and_permissions_to_session(session: SessionContainer): + # we add the user's roles to the user's session + await session.fetch_and_set_claim(UserRoleClaim) + + # we add the user's permissions to the user's session + await session.fetch_and_set_claim(PermissionClaim) +``` + + +```python +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim + + +def add_roles_and_permissions_to_session(session: SessionContainer): + # we add the user's roles to the user's session + session.sync_fetch_and_set_claim(UserRoleClaim) + + # we add the user's permissions to the user's session + session.sync_fetch_and_set_claim(PermissionClaim) +``` + + + + + +:::info[Multi Tenancy] Whilst roles and permissions share across apps, the association of roles to users is on a per tenant level. If using SDK functions to add a role to a user, you can also pass in a `tenantId` to the function. This tells SuperTokens to add the role for that user for that tenant. @@ -218,10 +447,9 @@ Note that if you associate a role to a user ID for a tenant, and that user ID do ## See also - - - - - - - + + + + + + diff --git a/docs/additional-verification/user-roles/introduction.mdx b/docs/additional-verification/user-roles/introduction.mdx index 0f243b1826..17e26d9175 100644 --- a/docs/additional-verification/user-roles/introduction.mdx +++ b/docs/additional-verification/user-roles/introduction.mdx @@ -1,62 +1,43 @@ --- -sidebar_position: 1 title: Introduction -hide_title: true -skip_llms_txt: true description: Manage user roles and permissions with SuperTokens. -page_type: overview -recipe: userroles -category: user-roles +sidebar: + order: 1 --- - -# User roles - ## Overview **SuperTokens** provides a way to set different levels of authorization control through the `User Roles` feature. With it, you can create roles and permissions, assign them to your users and limit access based on your application logic. -Basic User Roles Architecture + + Basic User Roles Architecture + ## Getting started You can go through the *Initial Setup* page for a quick tutorial on how to configure the feature. - - - - Quickstart - - - Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. - - - + + +Go through a quick tutorial that shows you how to add the **Email Verification** recipe to your application. + + ## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Protect frontend and backend routes - - - Limit the access to your frontend and backend routes based on roles and permissions. - - - - - Manage roles and permissions - - - Create new roles and permissions, assign them to users and manage them. - - - - - - + + +Limit the access to your frontend and backend routes based on roles and permissions. + + +Create new roles and permissions, assign them to users and manage them. + + diff --git a/docs/additional-verification/user-roles/meta.ts b/docs/additional-verification/user-roles/meta.ts new file mode 100644 index 0000000000..e87eccfe5c --- /dev/null +++ b/docs/additional-verification/user-roles/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "User Roles", + icon: "shield-user", + order: 4, +}); diff --git a/docs/additional-verification/user-roles/protecting-routes.mdx b/docs/additional-verification/user-roles/protecting-routes.mdx index 51bda315a9..ad3a098d1d 100644 --- a/docs/additional-verification/user-roles/protecting-routes.mdx +++ b/docs/additional-verification/user-roles/protecting-routes.mdx @@ -1,30 +1,22 @@ --- -sidebar_position: 5 title: Protect frontend and backend routes -hide_title: true -description: >- - Protect frontend and backend routes using SuperTokens session access tokens - and role-based validation. -page_type: guide -recipe: userroles -category: user-roles +description: Protect frontend and backend routes using SuperTokens session access tokens and role-based validation. +sidebar: + order: 5 --- - -# Protect frontend and backend routes - ## Overview To limit access to your application resources based on roles and permissions you have to use the `UserRoleClaim` inside the session validation logic. ## Before you start - +:::info[Access token guidance] -If you are implementing [**Unified Login**](/docs/authentication/unified-login/introduction), which uses **OAuth2 Access Tokens**, please check the [separate page](/docs/authentication/unified-login/verify-tokens) that shows you how to validate them. +If you are implementing [**Unified Login**](/authentication/unified-login/introduction), which uses **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to validate them. You have to check for the `roles` claim in the token payload. - +::: --- @@ -39,11 +31,20 @@ To perform the verification follow these steps: - Optionally, add a `PermissionClaim` validator to enforce a permission. - - - - - + + + + + + + + + + + + + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; import express from "express"; @@ -53,54 +54,52 @@ import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( - "/update-blog", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], - }), - async (req: SessionRequest, res) => { - // All validator checks have passed and the user is an admin. - } + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + async (req: SessionRequest, res) => { + // All validator checks have passed and the user is an admin. + }, ); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], - }), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // All validator checks have passed and the user is an admin. - } -}) + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // All validator checks have passed and the user is an admin. + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -109,65 +108,67 @@ import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ], + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], }), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { // All validator checks have passed and the user is an admin. -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; async function updateBlog(awsEvent: SessionEvent) { - // All validator checks have passed and the user is an admin. -}; + // All validator checks have passed and the user is an admin. +} exports.handler = verifySession(updateBlog, { - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import {SessionContext} from "supertokens-node/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); -router.post("/update-blog", verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - }), async (ctx: SessionContext, next) => { +router.post( + "/update-blog", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + async (ctx: SessionContext, next) => { // All validator checks have passed and the user is an admin. -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -176,123 +177,84 @@ import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; class SetRole { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/update-blog") - @intercept(verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })) - @response(200) - async handler() { - // All validator checks have passed and the user is an admin. - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + }), + ) + @response(200) + async handler() { + // All validator checks have passed and the user is an admin. + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; -// highlight-start export default async function setRole(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin"), - // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })(req, res, next); - }, - req, - res - ) - // All validator checks have passed and the user is an admin. -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserRoles from "supertokens-node/recipe/userroles"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // All validator checks have passed and the user is an admin. - return NextResponse.json({}) + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + // UserRoles.PermissionClaim.validators.includes("edit") + ], + })(req, res, next); }, - { - // highlight-start - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] - } - // highlight-end - }); + req, + res, + ); + // All validator checks have passed and the user is an admin. } ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; @Controller() export class ExampleController { - @Post('example') - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ - ...globalValidators, + @Post("example") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ + ...globalValidators, UserRoles.UserRoleClaim.validators.includes("admin"), // UserRoles.PermissionClaim.validators.includes("edit") - ]) - })) + ], + }), + ) async postExample(@Session() session: SessionContainer): Promise { // All validator checks have passed and the user is an admin. return true; } } ``` - - - - - - - - + + + + + + ```go import ( "net/http" @@ -307,12 +269,10 @@ import ( func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI).ServeHTTP(rw, r) }) } @@ -321,10 +281,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all validators have passed.. } ``` - - - - + + ```go import ( "net/http" @@ -342,12 +300,10 @@ func main() { // Wrap the API handler in session.VerifySession router.POST("/likecomment", verifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }), exampleAPI) } @@ -368,10 +324,8 @@ func exampleAPI(c *gin.Context) { // TODO: session is verified and all claim validators pass. } ``` - - - - + + ```go import ( "net/http" @@ -389,12 +343,10 @@ func main() { // Wrap the API handler in session.VerifySession r.Post("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)) } @@ -403,10 +355,8 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { } ``` - - - - + + ```go import ( "net/http" @@ -424,12 +374,10 @@ func main() { // Wrap the API handler in session.VerifySession router.HandleFunc("/likecomment", session.VerifySession(&sessmodels.VerifySessionOptions{ - // highlight-start OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, - // highlight-end }, exampleAPI)).Methods(http.MethodPost) } @@ -437,16 +385,13 @@ func exampleAPI(w http.ResponseWriter, r *http.Request) { // TODO: session is verified and all claim validators pass. } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer @@ -454,44 +399,36 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends( verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) )): # All validator checks have passed and the user has a verified email address pass ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) def like_comment(): # All validator checks have passed and the user has a verified email address pass ``` - - - - + + ```python from django.http import HttpRequest @@ -500,361 +437,356 @@ from supertokens_python.recipe.userroles import UserRoleClaim @verify_session( - # highlight-start # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, session, user_context: global_validators + \ [UserRoleClaim.validators.includes("admin")] - # highlight-end ) async def like_comment(request: HttpRequest): # All validator checks have passed and the user has a verified email address pass ``` + + + + + + + + + + + +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // All validator checks have passed and the user is an admin. + return NextResponse.json({}); + }, + { + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }, + ); +} +``` - - - - + + + + + ### Custom validation If you want to have more complex access control you can get the list of roles attached to the session and introduce your own logic. - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let app = express(); app.post("/update-blog", verifySession(), async (req: SessionRequest, res) => { - // highlight-start - const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); - - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) - } - // highlight-end - // user is an admin.. + const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); + + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); + } + // user is an admin.. }); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import {SessionRequest} from "supertokens-node/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/update-blog", - method: "post", - options: { - pre: [ - { - method: verifySession() - }, + path: "/update-blog", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); + + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, ], - }, - handler: async (req: SessionRequest, res) => { - // highlight-start - const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); - - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) - } - // highlight-end - // user is an admin.. + }); } -}) + // user is an admin.. + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; import { SessionRequest } from "supertokens-node/framework/fastify"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let fastify = Fastify(); -fastify.post("/update-blog", { +fastify.post( + "/update-blog", + { preHandler: verifySession(), -}, async (req: SessionRequest, res) => { - // highlight-start + }, + async (req: SessionRequest, res) => { const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); } - // highlight-end // user is an admin.. -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; async function updateBlog(awsEvent: SessionEvent) { - // highlight-start - const roles = await awsEvent.session!.getClaimValue(UserRoles.UserRoleClaim); - - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) - } - // highlight-end - // user is an admin.. -}; + const roles = await awsEvent.session!.getClaimValue(UserRoles.UserRoleClaim); + + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); + } + // user is an admin.. +} exports.handler = verifySession(updateBlog); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; import { SessionContext } from "supertokens-node/framework/koa"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/update-blog", verifySession(), async (ctx: SessionContext, next) => { - // highlight-start - const roles = await ctx.session!.getClaimValue(UserRoles.UserRoleClaim); - - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) - } - // highlight-end - // user is an admin.. + const roles = await ctx.session!.getClaimValue(UserRoles.UserRoleClaim); + + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); + } + // user is an admin.. }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; import Session from "supertokens-node/recipe/session"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; class UpdateBlog { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @post("/update-blog") - @intercept(verifySession()) - @response(200) - async handler() { - // highlight-start - const roles = await ((this.ctx as any).session as Session.SessionContainer).getClaimValue(UserRoles.UserRoleClaim); - - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) - } - // highlight-end - // user is an admin.. + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/update-blog") + @intercept(verifySession()) + @response(200) + async handler() { + const roles = await ((this.ctx as any).session as Session.SessionContainer).getClaimValue(UserRoles.UserRoleClaim); + + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); } + // user is an admin.. + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; export default async function updateBlog(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); - // highlight-start - const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); + const roles = await req.session!.getClaimValue(UserRoles.UserRoleClaim); - if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - await superTokensNextWrapper( - async (next) => { - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) + if (roles === undefined || !roles.includes("admin")) { + // this error tells SuperTokens to return a 403 to the frontend. + await superTokensNextWrapper( + async (next) => { + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, }, - req, - res - ) - } - // highlight-end - // user is an admin.. -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserRoles from "supertokens-node/recipe/userroles"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; -import { Error as STError } from "supertokens-node/recipe/session" - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const roles = await session!.getClaimValue(UserRoles.UserRoleClaim); - if (roles === undefined || !roles.includes("admin")) { - const error = new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }); - return NextResponse.json(error, { status: 403 }); - } - // user is an admin.. - return NextResponse.json({}) - }); + ], + }); + }, + req, + res, + ); + } + // user is an admin.. } - ``` - - - - -```tsx + + +```tsx check=false reason="application example imports local modules defined elsewhere" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; -import { Error as STError } from "supertokens-node/recipe/session" +import { Error as STError } from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise { - // highlight-start const roles = await session.getClaimValue(UserRoles.UserRoleClaim); if (roles === undefined || !roles.includes("admin")) { - // this error tells SuperTokens to return a 403 to the frontend. - throw new STError({ - type: "INVALID_CLAIMS", - message: "User is not an admin", - payload: [{ - id: UserRoles.UserRoleClaim.key - }] - }) + // this error tells SuperTokens to return a 403 to the frontend. + throw new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); } - // highlight-end // user is an admin.. return true; } } ``` - - - - - - - + + + + + + ```go import ( "net/http" @@ -885,7 +817,6 @@ func corsMiddleware(next http.Handler) http.Handler { } func postExample(w http.ResponseWriter, r *http.Request) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) @@ -897,10 +828,10 @@ func postExample(w http.ResponseWriter, r *http.Request) { }, }, r, w) if err != nil { - // TODO: send 500 error to client + http.Error(w, "Internal server error", http.StatusInternalServerError) } + return } - // highlight-end // User is an admin... } @@ -914,10 +845,8 @@ func contains(s []interface{}, e string) bool { return false } ``` - - - - + + ```go import ( "net/http" @@ -951,7 +880,6 @@ func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc { // This is the API handler. func postExample(c *gin.Context) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) @@ -963,10 +891,10 @@ func postExample(c *gin.Context) { }, }, c.Request, c.Writer) if err != nil { - // TODO: send 500 error to client + http.Error(c.Writer, "Internal server error", http.StatusInternalServerError) } + return } - // highlight-end // User is an admin... } @@ -980,10 +908,8 @@ func contains(s []interface{}, e string) bool { return false } ``` - - - - + + ```go import ( "net/http" @@ -1004,7 +930,6 @@ func main() { // This is the API handler. func postExample(w http.ResponseWriter, r *http.Request) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) @@ -1016,10 +941,10 @@ func postExample(w http.ResponseWriter, r *http.Request) { }, }, r, w) if err != nil { - // TODO: send 500 error to client + http.Error(w, "Internal server error", http.StatusInternalServerError) } + return } - // highlight-end } func contains(s []interface{}, e string) bool { @@ -1031,10 +956,8 @@ func contains(s []interface{}, e string) bool { return false } ``` - - - - + + ```go import ( "net/http" @@ -1056,7 +979,6 @@ func main() { // This is the API handler. func postExample(w http.ResponseWriter, r *http.Request) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(r.Context()) roles := sessionContainer.GetClaimValue(userrolesclaims.UserRoleClaim) @@ -1068,10 +990,10 @@ func postExample(w http.ResponseWriter, r *http.Request) { }, }, r, w) if err != nil { - // TODO: send 500 error to client + http.Error(w, "Internal server error", http.StatusInternalServerError) } + return } - // highlight-end } func contains(s []interface{}, e string) bool { @@ -1083,16 +1005,13 @@ func contains(s []interface{}, e string) bool { return false } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="route fragment assumes an existing framework application" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer @@ -1104,22 +1023,18 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.post("/update-blog") # type: ignore +@app.post("/update-blog") async def update_blog_api(session: SessionContainer = Depends(verify_session())): - # highlight-start roles = await session.get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) - # highlight-end ``` - - - - -```python + + +```python check=false reason="route fragment assumes an existing framework application" from flask import Flask, g from supertokens_python.recipe.session import SessionContainer @@ -1133,24 +1048,20 @@ from supertokens_python.recipe.userroles import UserRoleClaim app = Flask(__name__) -@app.route("/update-blog", methods=["POST"]) # type: ignore +@app.route("/update-blog", methods=["POST"]) @verify_session() def set_role_api(): - session: SessionContainer = g.supertokens # type: ignore - # highlight-start + session: SessionContainer = g.supertokens roles = session.sync_get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) - # highlight-end ``` - - - - -```python + + +```python check=false reason="initialization excerpt omits deployment connection config" from typing import cast from django.http import HttpRequest @@ -1166,334 +1077,300 @@ from supertokens_python.recipe.userroles import UserRoleClaim @verify_session() async def get_user_info_api(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore - # highlight-start + session: SessionContainer = cast(SessionContainer, request.supertokens) roles = await session.get_claim_value(UserRoleClaim) if roles is None or "admin" not in roles: raise_invalid_claims_exception( "User is not an admin", [ClaimValidationError(UserRoleClaim.key, None)] ) - # highlight-end ``` + + + + + + + + + + - - +```tsx check=false reason="application example imports local modules defined elsewhere" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserRoles from "supertokens-node/recipe/userroles"; +import { backendConfig } from "@/app/config/backend"; +import { Error as STError } from "supertokens-node/recipe/session"; + +SuperTokens.init(backendConfig()); +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const roles = await session!.getClaimValue(UserRoles.UserRoleClaim); + if (roles === undefined || !roles.includes("admin")) { + const error = new STError({ + type: "INVALID_CLAIMS", + message: "User is not an admin", + payload: [ + { + id: UserRoles.UserRoleClaim.key, + }, + ], + }); + return NextResponse.json(error, { status: 403 }); + } + // user is an admin.. + return NextResponse.json({}); + }); +} +``` - - + + + + + --- ## Protect frontend routes +:::caution[Backend authorization is mandatory] + +Frontend role and permission checks are for user experience only; they are not an authorization boundary. Always +enforce authorization on the backend before allowing access to a protected resource or operation. + +::: + On your frontend: 1. Verify that a session exists 2. Use the roles / permissions claim validators to enforce certain roles and permissions. 3. If the user doesn't have the right roles, the system shows an error message indicating they don't have access. - - - - - + - + + + ```tsx import React from "react"; -import { SessionAuth } from 'supertokens-auth-react/recipe/session'; -import { AccessDeniedScreen } from 'supertokens-auth-react/recipe/session/prebuiltui'; -import { UserRoleClaim, /*PermissionClaim*/ } from 'supertokens-auth-react/recipe/userroles'; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { AccessDeniedScreen } from "supertokens-auth-react/recipe/session/prebuiltui"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-auth-react/recipe/userroles"; const AdminRoute = (props: React.PropsWithChildren) => { - return ( - [ - ...globalValidators, UserRoleClaim.validators.includes("admin"), - ] - }> - {props.children} - - ); -} - -``` -Above, you create a generic component called `AdminRoute` which enforces that its child components render only if the user has the admin role. - -In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass a custom component to the `accessDeniedScreen` prop. - -:::note -You can extend the `AdminRoute` component to check for other types of validators as well. This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`). -::: - - -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - -```tsx -import Session from "supertokens-auth-react/recipe/session"; -import {UserRoleClaim} from "supertokens-auth-react/recipe/userroles" - -function ProtectedComponent() { - let claimValue = Session.useClaimValue(UserRoleClaim) - if (claimValue.loading || !claimValue.doesSessionExist) { - return null; - } - let roles = claimValue.value; - if (Array.isArray(roles) && roles.includes("admin")) { - // User is an admin - } else { - // User doesn't have any roles, or is not an admin.. - } -} + return ( + [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + ]} + > + {props.children} + + ); +}; ``` - - - - - + + ```tsx import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim, /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - - // highlight-start - let validationErrors = await Session.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - UserRoleClaim.validators.includes("admin"), - /* PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + /* PermissionClaim.validators.includes("modify") */ + ], + }); - if (validationErrors.length === 0) { - // user is an admin - return true; - } + if (validationErrors.length === 0) { + // user is an admin + return true; + } - for (const err of validationErrors) { - if (err.id === UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } + for (const err of validationErrors) { + if (err.id === UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; } ``` + + + + + +Above, you create a generic component called `AdminRoute` which enforces that its child components render only if the user has the admin role. + +In the `AdminRoute` component, the `SessionAuth` wrapper ensures that the session exists. The `UserRoleClaim` validator is also added to the `` component which checks if the validators pass or not. If all validation passes, the `props.children` component renders. If the claim validation has failed, it displays the `AccessDeniedScreen` component instead of rendering the children. You can also pass a custom component to the `accessDeniedScreen` prop. + +:::note[You can extend the `AdminRoute` component to check for other types of validators as well. This component can then reuse to protect all your app's components (In this case, you may want to rename this component to something more appropriate, like `ProtectedRoute`).] +::: +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + - We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. - The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). - We can also add a `PermissionClaim` validator to enforce a permission. - - If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + + + + ```tsx -import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; +import Session from "supertokens-auth-react/recipe/session"; +import { UserRoleClaim } from "supertokens-auth-react/recipe/userroles"; -async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let roles = await Session.getClaimValue({claim: UserRoleClaim}); - if (Array.isArray(roles) && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end - } - // either a session does not exist, or the user is not an admin - return false +function ProtectedComponent() { + let claimValue = Session.useClaimValue(UserRoleClaim); + if (claimValue.loading || !claimValue.doesSessionExist) { + return null; + } + let roles = claimValue.value; + if (Array.isArray(roles) && roles.includes("admin")) { + // User is an admin + } else { + // User doesn't have any roles, or is not an admin.. + } } ``` - - - - - - - - - - - - - - - - + + ```tsx import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim, /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; +import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - - // highlight-start - let validationErrors = await Session.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - UserRoleClaim.validators.includes("admin"), - /* PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end - - if (validationErrors.length === 0) { - // user is an admin - return true; - } - - for (const err of validationErrors) { - if (err.id === UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } + if (await Session.doesSessionExist()) { + let roles = await Session.getClaimValue({ claim: UserRoleClaim }); + if (Array.isArray(roles) && roles.includes("admin")) { + // User is an admin + return true; } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false + } + // either a session does not exist, or the user is not an admin + return false; } ``` + + - - + -- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. -- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). -- We can also add a `PermissionClaim` validator to enforce a permission. + -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: - + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; -import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; +import { UserRoleClaim /*PermissionClaim*/ } from "supertokens-web-js/recipe/userroles"; async function shouldLoadRoute(): Promise { - if (await Session.doesSessionExist()) { - // highlight-start - let roles = await Session.getClaimValue({claim: UserRoleClaim}); - if (roles !== undefined && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end - } - // either a session does not exist, or the user is not an admin - return false -} -``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensUserRoles from "supertokens-web-js-script/recipe/userroles"; -async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - - // highlight-start - let validationErrors = await supertokensSession.validateClaims({ - overrideGlobalClaimValidators: (globalValidators) => - [...globalValidators, - supertokensUserRoles.UserRoleClaim.validators.includes("admin"), - /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ - ] - }); - // highlight-end + if (await Session.doesSessionExist()) { + let validationErrors = await Session.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + UserRoleClaim.validators.includes("admin"), + /* PermissionClaim.validators.includes("modify") */ + ], + }); - if (validationErrors.length === 0) { - // user is an admin - return true; - } + if (validationErrors.length === 0) { + // user is an admin + return true; + } - for (const err of validationErrors) { - if (err.id === supertokensUserRoles.UserRoleClaim.id) { - // user roles claim check failed - } else { - // some other claim check failed (from the global validators list) - } - } + for (const err of validationErrors) { + if (err.id === UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } } - // either a session does not exist, or one of the validators failed. - // so we do not allow access to this page. - return false + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; } ``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +async function shouldLoadRoute(): Promise { + if (await supertokensSession.doesSessionExist()) { + let validationErrors = await supertokensSession.validateClaims({ + overrideGlobalClaimValidators: (globalValidators) => [ + ...globalValidators, + supertokensUserRoles.UserRoleClaim.validators.includes("admin"), + /* supertokensUserRoles.PermissionClaim.validators.includes("modify") */ + ], + }); -- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. -- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). -- We can also add a `PermissionClaim` validator to enforce a permission. - -If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + if (validationErrors.length === 0) { + // user is an admin + return true; + } -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensUserRoles from "supertokens-web-js-script/recipe/userroles"; -async function shouldLoadRoute(): Promise { - if (await supertokensSession.doesSessionExist()) { - // highlight-start - let roles = await supertokensSession.getClaimValue({claim: supertokensUserRoles.UserRoleClaim}); - if (roles !== undefined && roles.includes("admin")) { - // User is an admin - return true; - } - // highlight-end + for (const err of validationErrors) { + if (err.id === supertokensUserRoles.UserRoleClaim.id) { + // user roles claim check failed + } else { + // some other claim check failed (from the global validators list) + } } - // either a session does not exist, or the user is not an admin - return false + } + // either a session does not exist, or one of the validators failed. + // so we do not allow access to this page. + return false; } ``` - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function getRole() { - if (await SuperTokens.doesSessionExist()) { - - // highlight-start - let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; + if (await SuperTokens.doesSessionExist()) { + let roles: string[] = (await SuperTokens.getAccessTokenPayloadSecurely())["st-role"].v; - if (roles.includes("admin")) { - // TODO.. - } else { - // TODO.. - } - // highlight-end + if (roles.includes("admin")) { + // TODO.. + } else { + // TODO.. } + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -1511,11 +1388,8 @@ class MainApplication: Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -1532,11 +1406,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -1558,24 +1429,88 @@ Future checkIfUserIsAnAdmin() async { } } ``` + + + + - + + + + +
- - +- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. +- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). +- We can also add a `PermissionClaim` validator to enforce a permission. + +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + +
+
+ +- We call the `validateClaims` function with the `UserRoleClaim` validator which makes sure that the user has an `admin` role. +- The `globalValidators` represents other validators that apply to all calls to the `validateClaims` function. This may include a validator that enforces that the user has verified their email (if enabled by you). +- We can also add a `PermissionClaim` validator to enforce a permission. + +If you want to have more complex access control, you can get the roles list from the session as follows, and check the list yourself: + +
+
+
+ + + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; +import { UserRoleClaim } from "supertokens-web-js/recipe/userroles"; + +async function shouldLoadRoute(): Promise { + if (await Session.doesSessionExist()) { + let roles = await Session.getClaimValue({ claim: UserRoleClaim }); + if (roles !== undefined && roles.includes("admin")) { + // User is an admin + return true; + } + } + // either a session does not exist, or the user is not an admin + return false; +} +``` + + +```tsx check=false reason="script-tag example relies on globals provided by loaded SuperTokens bundles" +async function shouldLoadRoute(): Promise { + if (await supertokensSession.doesSessionExist()) { + let roles = await supertokensSession.getClaimValue({ claim: supertokensUserRoles.UserRoleClaim }); + if (roles !== undefined && roles.includes("admin")) { + // User is an admin + return true; + } + } + // either a session does not exist, or the user is not an admin + return false; +} +``` + + + + + -
-
+ --- ## See also - - - - - - + + + + + + diff --git a/docs/additional-verification/user-roles/role-management-actions.mdx b/docs/additional-verification/user-roles/role-management-actions.mdx index 41893a04ec..72cf68340f 100644 --- a/docs/additional-verification/user-roles/role-management-actions.mdx +++ b/docs/additional-verification/user-roles/role-management-actions.mdx @@ -1,22 +1,10 @@ --- -sidebar_position: 4 title: Role management actions -hide_title: true -description: >- - Discover how to perform common actions that can be use to manage roles and - permissions. -page_type: guide -recipe: userroles -category: user-roles +description: Discover how to perform common actions that can be use to manage roles and permissions. +sidebar: + order: 4 --- - -import CreateRole from "./_blocks/create-role.mdx"; -import AssignRoleToUser from "./_blocks/assign-role-to-user.mdx"; -import AssignRoleToSession from "./_blocks/assign-role-to-session.mdx"; - -# Role management actions - ## Overview @@ -25,10 +13,8 @@ Actions like listing roles, creating permissions, or checking which roles you as ## Before you start -:::info - -You can also perform most of the actions outlined on this page from the user management dashboard. -To know more about how to use it check [the documentation](/docs/post-authentication/dashboard/user-management) +:::info[You can also perform most of the actions outlined on this page from the user management dashboard.] +To know more about how to use it check [the documentation](/post-authentication/dashboard/user-management) ::: @@ -36,89 +22,153 @@ To know more about how to use it check [the documentation](/docs/post-authentica ## Create a role - + + +Create Role + + + + + +```tsx +import UserRoles from "supertokens-node/recipe/userroles"; + +async function createRole() { + const response = await UserRoles.createNewRoleOrAddPermissions("user", ["read"]); + + if (response.createdNewRole === false) { + // The role already exists + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/userroles" +) + +func createRole() { + resp, err := userroles.CreateNewRoleOrAddPermissions("user", []string{ + "read", + }, nil) + + if err != nil { + // TODO: Handle error + return + } + if resp.OK.CreatedNewRole == false { + // The role already exists + } +} +``` + + + + +```python +from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions + +async def create_role(): + res = await create_new_role_or_add_permissions("user", ["read"]) + if not res.created_new_role: + # The role already existed + pass + +``` + + +```python +from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions + +def create_role(): + res = create_new_role_or_add_permissions("user", ["read"]) + if not res.created_new_role: + # The role already existed + pass + +``` + + + + +```bash +curl --location --request PUT '/recipe/role' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "role": "user", + "permissions": [ + "read" + ] +}' + +``` + + + + + --- ## List roles - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getAllRoles() { - // highlight-start - const roles: string[] = (await UserRoles.getAllRoles()).roles; - // highlight-end + const roles: string[] = (await UserRoles.getAllRoles()).roles; } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getAllRoles() { - // highlight-start response, err := userroles.GetAllRoles(nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import get_all_roles async def create_role(): - # highlight-next-line _ = (await get_all_roles()).roles ``` - - - - - + + ```python from supertokens_python.recipe.userroles.syncio import get_all_roles def create_role(): - # highlight-next-line _ = get_all_roles().roles ``` - - - - - - - - - + + + + ```bash curl --location --request GET 'http://localhost:3567/recipe/roles' \ ---header 'api-key: ^{coreInfo.key}' +--header 'api-key: ' ``` - - - - + + --- @@ -126,34 +176,28 @@ curl --location --request GET 'http://localhost:3567/recipe/roles' \ You can delete any role you have created, if the role you are trying to delete does not exist then this has no effect. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function deleteRole() { - // highlight-start - // Delete the user role - const response = await UserRoles.deleteRole("user"); - - if (!response.didRoleExist) { - // There was no such role - } - // highlight-end + // Delete the user role + const response = await UserRoles.deleteRole("user"); + + if (!response.didRoleExist) { + // There was no such role + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func deleteRole() { - // highlight-start // Delete the user role response, err := userroles.DeleteRole("user", nil) if err != nil { @@ -164,62 +208,48 @@ func deleteRole() { if response.OK.DidRoleExist == false { // There was no such role } - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import delete_role async def delete_role_function(): - # highlight-start res = await delete_role("user") if res.did_role_exist: # The role actually existed pass - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.userroles.syncio import delete_role def delete_role_function(): - # highlight-start res = delete_role("user") if res.did_role_exist: # The role actually existed pass - # highlight-end ``` - - - - - - - + + + + ```bash curl --location --request POST 'http://localhost:3567/recipe/role/remove' \ ---header 'api-key: ^{coreInfo.key}' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "admin" }' ``` - - - + + --- @@ -228,78 +258,60 @@ curl --location --request POST 'http://localhost:3567/recipe/role/remove' \ The SDK function only adds missing permissions and does not have any effect on permissions that are already assigned to a role. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function addPermissionForRole() { - // highlight-start - // Add the "write" permission to the "user" role - await UserRoles.createNewRoleOrAddPermissions("user", ["write"]); - // highlight-end + // Add the "write" permission to the "user" role + await UserRoles.createNewRoleOrAddPermissions("user", ["write"]); } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func addPermissionForRole() { - // highlight-start // Add the write permission to the user role _, err := userroles.CreateNewRoleOrAddPermissions("user", []string{"write"}, nil) if err != nil { // TODO: Handle error return } - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import create_new_role_or_add_permissions async def add_permission_for_role(): - # highlight-next-line await create_new_role_or_add_permissions("user", ["write"]) ``` - - - - - + + ```python from supertokens_python.recipe.userroles.syncio import create_new_role_or_add_permissions def add_permission_for_role(): - # highlight-next-line create_new_role_or_add_permissions("user", ["write"]) ``` + + + + - - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] In a multi-tenant setup, roles, and permissions share across all tenants. This means that you can create a role and add permissions to it once, and reuse that role across any tenant in your app. @@ -311,34 +323,28 @@ In a multi-tenant setup, roles, and permissions share across all tenants. This m To remove one or more permissions from a role, first create the role before you use this function. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function removePermissionFromRole() { - // highlight-start - // Remove the "write" permission to the "user" role - const response = await UserRoles.removePermissionsFromRole("user", ["write"]); - - if (response.status === "UNKNOWN_ROLE_ERROR") { - // No such role exists - } - // highlight-end + // Remove the "write" permission to the "user" role + const response = await UserRoles.removePermissionsFromRole("user", ["write"]); + + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func removePermissionFromRole() { - // highlight-start // Remove the write permission to the user role response, err := userroles.RemovePermissionsFromRole("user", []string{"write"}, nil) if err != nil { @@ -349,53 +355,40 @@ func removePermissionFromRole() { if response.UnknownRoleError != nil { // No such role exists } - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import remove_permissions_from_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def remove_permission_from_role_func(): - # highlight-start res = await remove_permissions_from_role("user", ["write"]) if isinstance(res, UnknownRoleError): # No such role exists pass - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.userroles.syncio import remove_permissions_from_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def remove_permission_from_role_func(): - # highlight-start res = remove_permissions_from_role("user", ["write"]) if isinstance(res, UnknownRoleError): # No such role exists pass - # highlight-end ``` - - - - - - - + + + + --- @@ -403,36 +396,30 @@ def remove_permission_from_role_func(): Get a list of all permissions assigned to a role. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getPermissionsForRole() { - // highlight-start - const response = await UserRoles.getPermissionsForRole("user"); + const response = await UserRoles.getPermissionsForRole("user"); - if (response.status === "UNKNOWN_ROLE_ERROR") { - // No such role exists - return; - } + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + return; + } - const permissions: string[] = response.permissions; - // highlight-end + const permissions: string[] = response.permissions; } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getPermissionsForRole() { - // highlight-start // const response = await UserRoles.getPermissionsForRole("user"); response, err := userroles.GetPermissionsForRole("user", nil) if err != nil { @@ -446,56 +433,44 @@ func getPermissionsForRole() { } _ = response.OK.Permissions - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def remove_permission_from_role(): - # highlight-start res = await get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions - # highlight-end ``` - - - + + ```python from supertokens_python.recipe.userroles.syncio import get_permissions_for_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def remove_permission_from_role(): - # highlight-start res = get_permissions_for_role("user") if isinstance(res, UnknownRoleError): # No such role exists return _ = res.permissions - # highlight-end ``` - - - - - - - + + + + --- @@ -503,93 +478,240 @@ def remove_permission_from_role(): Get a list of all the roles assigned a specific permission. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getRolesWithPermission() { - // highlight-start - const response = await UserRoles.getRolesThatHavePermission("write"); - const roles: string[] = response.roles; - // highlight-end + const response = await UserRoles.getRolesThatHavePermission("write"); + const roles: string[] = response.roles; } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getRolesWithPermission() { - // highlight-start response, err := userroles.GetRolesThatHavePermission("write", nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import get_roles_that_have_permission async def get_roles_with_permission(): - # highlight-start res = await get_roles_that_have_permission("write") _ = res.roles - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.userroles.syncio import get_roles_that_have_permission def get_roles_with_permission(): - # highlight-start res = get_roles_that_have_permission("write") _ = res.roles - # highlight-end ``` + + + + - - +--- - +## Assign roles to a user - ---- + + +```tsx +import UserRoles from "supertokens-node/recipe/userroles"; -## Assign roles to a user +async function addRoleToUser(userId: string) { + const response = await UserRoles.addRoleToUser("public", userId, "user"); + + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + return; + } + if (response.didUserAlreadyHaveRole === true) { + // The user already had the role + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/userroles" +) + +func addRoleToUser(userId string) { + response, err := userroles.AddRoleToUser("public", userId, "user", nil) + if err != nil { + // TODO: Handle error + return + } + + if response.UnknownRoleError != nil { + // No such role exists + return + } + + if response.OK.DidUserAlreadyHaveRole { + // The user already had the role + } +} +``` + + + + +```python +from supertokens_python.recipe.userroles.asyncio import add_role_to_user +from supertokens_python.recipe.userroles.interfaces import UnknownRoleError + + +async def add_role_to_user_func(user_id: str): + role = "user" + res = await add_role_to_user("public", user_id, role) + if isinstance(res, UnknownRoleError): + # No such role exists + return + + if res.did_user_already_have_role: + # User already had this role + pass +``` + + +```python +from supertokens_python.recipe.userroles.syncio import add_role_to_user +from supertokens_python.recipe.userroles.interfaces import UnknownRoleError + + +def add_role_to_user_func(user_id: str): + role = "user" + res = add_role_to_user("public", user_id, role) + if isinstance(res, UnknownRoleError): + # No such role exists + return + + if res.did_user_already_have_role: + # User already had this role + pass - +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/user/role' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "userId": "fa7a0841-b533-4478-95533-0fde890c3483", + "role": "user" +}' +``` + + ## Assign roles to a session - + + +```tsx +import { UserRoleClaim, PermissionClaim } from "supertokens-node/recipe/userroles"; +import { SessionContainer } from "supertokens-node/recipe/session"; + +async function addRolesAndPermissionsToSession(session: SessionContainer) { + // we add the user's roles to the user's session + await session.fetchAndSetClaim(UserRoleClaim); + // we add the permissions of a user to the user's session + await session.fetchAndSetClaim(PermissionClaim); +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" + "github.com/supertokens/supertokens-golang/recipe/userroles/userrolesclaims" +) + +func addRolesAndPermissionsToSession(session sessmodels.SessionContainer) error { + // we add the user's roles to the user's session + err := session.FetchAndSetClaim(userrolesclaims.UserRoleClaim) + if err != nil { + return err + } + + // we add the user's permissions to the user's session + err = session.FetchAndSetClaim(userrolesclaims.PermissionClaim) + if err != nil { + return err + } -:::info Multi Tenancy + return nil +} +``` + + + + +```python +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim + + +async def add_roles_and_permissions_to_session(session: SessionContainer): + # we add the user's roles to the user's session + await session.fetch_and_set_claim(UserRoleClaim) + + # we add the user's permissions to the user's session + await session.fetch_and_set_claim(PermissionClaim) +``` + + +```python +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.userroles import UserRoleClaim, PermissionClaim + + +def add_roles_and_permissions_to_session(session: SessionContainer): + # we add the user's roles to the user's session + session.sync_fetch_and_set_claim(UserRoleClaim) + + # we add the user's permissions to the user's session + session.sync_fetch_and_set_claim(PermissionClaim) +``` + + + + + + +:::info[Multi Tenancy] Whilst roles and permissions share across apps, the association of roles to users is on a per-tenant level. If using SDK functions to add a role to a user, you can also pass in a `tenantId` to the function. This tells SuperTokens to add the role for that user for that tenant. In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. You can fetch the user's `tenantId` from their current session, or from their user object (which you can fetch using their `userId`). @@ -603,34 +725,31 @@ Note that if you associate a role to a user ID for a tenant, and that user ID do You can remove roles from a user. The system removes the role you provide only if the user previously had that role. - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; -import { SessionContainer } from "supertokens-node/recipe/session" +import { SessionContainer } from "supertokens-node/recipe/session"; async function removeRoleFromUserAndTheirSession(session: SessionContainer) { - const response = await UserRoles.removeUserRole(session.getTenantId(), session.getUserId(), "user"); - - if (response.status === "UNKNOWN_ROLE_ERROR") { - // No such role exists - return; - } - - if (response.didUserHaveRole === false) { - // The user was never assigned the role - } else { - // We also want to update the session of this user to reflect this change. - await session.fetchAndSetClaim(UserRoles.UserRoleClaim); - await session.fetchAndSetClaim(UserRoles.PermissionClaim); - } + const response = await UserRoles.removeUserRole(session.getTenantId(), session.getUserId(), "user"); + + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + return; + } + + if (response.didUserHaveRole === false) { + // The user was never assigned the role + } else { + // We also want to update the session of this user to reflect this change. + await session.fetchAndSetClaim(UserRoles.UserRoleClaim); + await session.fetchAndSetClaim(UserRoles.PermissionClaim); + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" @@ -659,13 +778,10 @@ func removeRoleFromUserAndTheirSession(session sessmodels.SessionContainer) { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import remove_user_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError @@ -686,10 +802,8 @@ async def remove_role_from_user_and_their_session(session: SessionContainer): await session.fetch_and_set_claim(UserRoleClaim) await session.fetch_and_set_claim(PermissionClaim) ``` - - - - + + ```python from supertokens_python.recipe.userroles.syncio import remove_user_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError @@ -710,29 +824,23 @@ def remove_role_from_user_and_their_session(session: SessionContainer): session.sync_fetch_and_set_claim(UserRoleClaim) session.sync_fetch_and_set_claim(PermissionClaim) ``` - - - - - - - - + + + + ```bash curl --location --request POST 'http://localhost:3567/recipe/user/role/remove' \ ---header 'api-key: ^{coreInfo.key}' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "fa7a0841-b533-4478-95533-0fde890c3483", "role": "user" }' ``` + + - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] When using the multi-tenancy feature, in the previous snippets, only the user's role for the tenant they used to log in gets removed. That's the one stored in the session. @@ -743,83 +851,62 @@ You can pass in another tenant ID if you like, or call the function above for al ## List the roles of a user - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getRolesForUser(userId: string) { - // highlight-start - const response = await UserRoles.getRolesForUser("public", userId); - const roles: string[] = response.roles; - // highlight-end + const response = await UserRoles.getRolesForUser("public", userId); + const roles: string[] = response.roles; } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getRolesForUser(userId string) { - // highlight-start response, err := userroles.GetRolesForUser("public", userId, nil) if err != nil { // TODO: Handle error return } _ = response.OK.Roles - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import get_roles_for_user async def get_roles_for_user_func(user_id: str): - # highlight-next-line _ = (await get_roles_for_user("public", user_id)).roles ``` - - - - - + + ```python from supertokens_python.recipe.userroles.syncio import get_roles_for_user def get_roles_for_user_func(user_id: str): - # highlight-next-line _ = get_roles_for_user("public", user_id).roles ``` - - - - - - - - + + + + ```bash curl --location --request GET 'http://localhost:3567/recipe/user/roles?userId=fa7a0841-b533-4478-95533-0fde890c3483' \ ---header 'api-key: ^{coreInfo.key}' +--header 'api-key: ' ``` + + - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. You can fetch the user's `tenantId` from their current session, or from their user object (which you can fetch using their `userId`). ::: @@ -828,36 +915,30 @@ In the code examples above, the `"public"` `tenantId` appears, which is the defa ## List the users of a role - - - + + ```tsx import UserRoles from "supertokens-node/recipe/userroles"; async function getUsersThatHaveRole(role: string) { - // highlight-start - const response = await UserRoles.getUsersThatHaveRole("public", role); + const response = await UserRoles.getUsersThatHaveRole("public", role); - if (response.status === "UNKNOWN_ROLE_ERROR") { - // No such role exists - return; - } + if (response.status === "UNKNOWN_ROLE_ERROR") { + // No such role exists + return; + } - const users: string[] = response.users; - // highlight-end + const users: string[] = response.users; } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/userroles" ) func getUsersThatHaveRole(role string) { - // highlight-start response, err := userroles.GetUsersThatHaveRole("public", role, nil) if err != nil { // TODO: Handle error @@ -870,69 +951,52 @@ func getUsersThatHaveRole(role string) { } _ = response.OK.Users - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.userroles.asyncio import get_users_that_have_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError async def get_users_that_have_role_func(role: str): - # highlight-start res = await get_users_that_have_role("public", role) if isinstance(res, UnknownRoleError): # No such role exists return _ = res.users - # highlight-end ``` - - - - - + + ```python from supertokens_python.recipe.userroles.syncio import get_users_that_have_role from supertokens_python.recipe.userroles.interfaces import UnknownRoleError def get_users_that_have_role_func(role: str): - # highlight-start res = get_users_that_have_role("public", role) if isinstance(res, UnknownRoleError): # No such role exists return _ = res.users - # highlight-end ``` - - - - - - - - + + + + ```bash curl --location --request GET 'http://localhost:3567/recipe/role/users?role=user' \ ---header 'api-key: ^{coreInfo.key}' +--header 'api-key: ' ``` + + - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] In the code examples above, the `"public"` `tenantId` appears, which is the default `tenantId` for users. This returns the list of users that have that role in the `"public"` tenant. You can also pass in a different tenant ID, or call the function in a loop with all the tenants that exist in your app. @@ -943,8 +1007,8 @@ You can also pass in a different tenant ID, or call the function in a loop with ## See also - - - - - + + + + + diff --git a/docs/authentication/_category_.json b/docs/authentication/_category_.json deleted file mode 100644 index 54ad7957a5..0000000000 --- a/docs/authentication/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Authentication", - "collapsible": true, - "collapsed": true, - "customProps": { - "isMainCategory": true - }, - "position": 3 -} diff --git a/docs/authentication/ai-authentication.mdx b/docs/authentication/ai-authentication.mdx index 3df644ce5e..ab3c951e61 100644 --- a/docs/authentication/ai-authentication.mdx +++ b/docs/authentication/ai-authentication.mdx @@ -1,30 +1,24 @@ --- title: MCP Authentication -hide_title: true -sidebar_position: 8 -pagination_next: null -pagination_prev: null -description: >- - Authenticate MCP server actions using the SuperTokens MCP plugin. -category: authentication -recipe: oauth2 -page_type: tutorial +description: Authenticate MCP server actions using the SuperTokens MCP plugin. +sidebar: + badge: Beta + order: 90 --- -# Authenticate MCP Servers - ## Overview This guide explains how to authenticate Model Context Protocol (MCP) Servers using **SuperTokens**. +For the public, read-only SuperTokens documentation MCP server, see [Build with AI Tools](/integrate-with-ai). This guide is for MCP servers that you build and host. The instructions make use of the `plugins` functionality. It is a new way to abstract common functionalities into a reusable package. ## Before you start - + The [MCP authentication flow](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) complies with the OAuth2 specifications. -This means that you will have to use the `OAuth2` recipe in your configuration which is a paid feature. +This means that you will have to use the `OAuth2` recipe in your configuration. The functionality is only available alongside the `node` SDK at the moment. Keep in mind that the feature is currently in beta and might be subject to breaking changes. @@ -35,23 +29,23 @@ Keep in mind that the feature is currently in beta and might be subject to break Add the `supertokens-mcp-plugin` package to your project. - - - ```bash - npm i -s supertokens-mcp-plugin - ``` - - - ```bash - yarn add supertokens-mcp-plugin - ``` - - - ```bash - pnpm add supertokens-auth-react supertokens-web-js - ``` - - + + +```bash +npm i -s supertokens-mcp-plugin +``` + + +```bash +yarn add supertokens-mcp-plugin +``` + + +```bash +pnpm add supertokens-mcp-plugin +``` + + ### 2. Add the MCP server @@ -59,37 +53,31 @@ Add the `supertokens-mcp-plugin` package to your project. Use the `SuperTokensMcpServer` class when in your implementation. The class extends the base MCP server exposed by the `@modelcontextprotocol/sdk`, and adds custom authentication logic on top of it. -You can authorize the client requests in two different ways. -By using the standard [claim validators](/docs/additional-verification/session-verification/claim-validation). -Or you can write your own custom validation logic in the `validateTokenPayload` function. +You can authorize the client requests in two different ways. +By using the standard [claim validators](/additional-verification/session-verification/claim-validation). +Or you can write your own custom validation logic in the `validateTokenPayload` function. The authentication state can be accessed inside a tool call through the second function argument, `extra.authInfo`. ```ts -import UserRoles, { UserRoleClaim } from "supertokens-node/recipe/userroles"; +import { UserRoleClaim } from "supertokens-node/recipe/userroles"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; -import type { TypeInput } from "supertokens-node/types"; -import { z } from "zod"; -import SuperTokensMcpPlugin, { - SuperTokensMcpServer, -} from "supertokens-mcp-plugin"; -import SuperTokens from "supertokens-node"; +import SuperTokensMcpPlugin, { SuperTokensMcpServer } from "supertokens-mcp-plugin"; const server = new SuperTokensMcpServer({ name: "example-mcp", version: "1.0.0", path: "/mcp", validateTokenPayload: async (_accessTokenPayload, _userContext) => { - // You can check the acccess token payload for any specific values + // You can check the access token payload for any specific values return { status: "OK", }; }, - // By default, you can use calim validators to determine who can access the MCP server + // You can use claim validators to determine who can access the MCP server claimValidators: [UserRoleClaim.validators.includes("admin")], }); - server.registerTool( "session-info", { @@ -105,9 +93,8 @@ server.registerTool( }, ], }; - } + }, ); - ``` ### 3. Update the SDK initialization code @@ -117,12 +104,15 @@ This way, the SDK middleware will expose your new endpoint and authenticate each ```ts import supertokens from "supertokens-node"; -import SuperTokensMcpPlugin, { - SuperTokensMcpServer, -} from "supertokens-mcp-plugin"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +import SuperTokensMcpPlugin, { SuperTokensMcpServer } from "supertokens-mcp-plugin"; // The server that you have previously created -const server = new SuperTokensMcpServer({}); +const server = new SuperTokensMcpServer({ + name: "example-mcp", + version: "1.0.0", + path: "/mcp", +}); supertokens.init({ supertokens: { @@ -150,5 +140,3 @@ supertokens.init({ }, }); ``` - - diff --git a/docs/authentication/email-password/_category_.json b/docs/authentication/email-password/_category_.json deleted file mode 100644 index ac15286c75..0000000000 --- a/docs/authentication/email-password/_category_.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "label": "Email Password", - "position": 1.2, - "customProps": { - "childrenOrder": [ - "introduction", - "callbacks", - "password-hashing", - "sing-in-form", - "sign-up-form", - "password-managers", - "password-reset" - ] - } -} diff --git a/docs/authentication/email-password/customize-the-sign-in-form.mdx b/docs/authentication/email-password/customize-the-sign-in-form.mdx index 9fa748ce28..8ef43a8c2d 100644 --- a/docs/authentication/email-password/customize-the-sign-in-form.mdx +++ b/docs/authentication/email-password/customize-the-sign-in-form.mdx @@ -1,97 +1,81 @@ --- title: Customize the Sign In form -hide_title: true -sidebar_position: 2 -description: >- - Customize form fields in the sign in component by modifying labels, - placeholders, default values, and error messages. -page_type: guide -recipe: emailpassword -category: email-password +description: Customize form fields in the sign in component by modifying labels, placeholders, default values, and error messages. +sidebar: + order: 20 --- - -# Customize form fields - - ## Before you start -The following instructions are only relevant if you are using the pre-built UI components. +The following instructions are only relevant if you are using the pre-built UI components. If you have created your own authentication UI, you can skip this guide. -## Modify labels and placeholders +## Modify labels and placeholders To change the labels and placeholders of the fields update the `formFields` property, in the recipe configuration. - - - - -```tsx preview="/img/emailpassword/signin-with-default-values.png" + + +```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signInForm: { - // highlight-start - formFields: [{ - id: "email", - label: "customFieldName", - placeholder: "Custom value" - }] - // highlight-end - } - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "customFieldName", + placeholder: "Custom value", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Prebuilt form UI with custom labels and placeholder" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signInForm: { - // highlight-start - formFields: [{ - id: "email", - label: "customFieldName", - placeholder: "Custom value" - }] - // highlight-end - } - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "customFieldName", + placeholder: "Custom value", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + --- @@ -100,74 +84,68 @@ supertokensUIInit({ Add a `getDefaultValue` option in the `formFields` configuration to pre-fill the inputs. Keep in mind that the function needs to return a string. - - - -```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Pre-built sign in form UI with default values for fields" + + +```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signInForm: { - formFields: [{ - id: "email", - label: "Your Email", - // highlight-start - getDefaultValue: () => "john.doe@gmail.com" - // highlight-end - }] - } - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "Your Email", + getDefaultValue: () => "john.doe@gmail.com", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signin-with-default-values.png" previewAlt="Pre-built sign in form UI with default values for fields" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signInForm: { - formFields: [{ - id: "email", - label: "Your Email", - // highlight-start - getDefaultValue: () => "john.doe@gmail.com" - // highlight-end - }] - } - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "Your Email", + getDefaultValue: () => "john.doe@gmail.com", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + --- @@ -176,95 +154,89 @@ supertokensUIInit({ When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. - - - -```tsx preview="/img/emailpassword/signin-with-custom-error-msg.png" previewAlt="Pre-built sign in UI with custom error message" + + +```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signInForm: { - formFields: [{ - id: "email", - label: "Your Email", - placeholder: "Email", - // highlight-start - nonOptionalErrorMsg: "Please add your email" - // highlight-end - }] - } - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "Your Email", + placeholder: "Email", + nonOptionalErrorMsg: "Please add your email", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signin-with-custom-error-msg.png" previewAlt="Pre-built sign in UI with custom error message" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signInForm: { - formFields: [{ - id: "email", - label: "Your Email", - placeholder: "Email", - // highlight-start - nonOptionalErrorMsg: "Please add your email" - // highlight-end - }] - } - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "Your Email", + placeholder: "Email", + nonOptionalErrorMsg: "Please add your email", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + --- -## Custom field validators +## Custom field validators To add custom validation logic to the sign in form, update the sign up form configuration. The `email` and `password` fields validation synchronizes between the two forms. -Check the [sign up form instructions](/docs/authentication/email-password/customize-the-sign-up-form#change-the-default-email-and-password-validators) for more details. +Check the [sign up form instructions](/authentication/email-password/customize-the-sign-up-form#change-the-default-email-and-password-validators) for more details. --- ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/email-password/customize-the-sign-up-form.mdx b/docs/authentication/email-password/customize-the-sign-up-form.mdx index 5be0eadeba..5b0cad39f6 100644 --- a/docs/authentication/email-password/customize-the-sign-up-form.mdx +++ b/docs/authentication/email-password/customize-the-sign-up-form.mdx @@ -1,23 +1,16 @@ --- title: Customize the Sign Up Form -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 description: Customize the sign up form by adding new fields or modifying existing ones. -page_type: guide -recipe: emailpassword -category: email-password +sidebar: + order: 30 --- -# Customize the sign up form - - ## Before you start The next instructions assume that you have a working application that uses **SuperTokens** for authentication. -If not, please refer to the [quickstart guide](/docs/quickstart/frontend-setup) and then return here. +If not, please refer to the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) and then return here. - + ## Add extra fields @@ -27,85 +20,86 @@ Then, when the sing up payload arrives on the backend, you should add a way to p ### 1. Add the new fields to the UI - + + You first need to add the new fields to your sign up interface. Given that you are using a custom implementation, the steps vary based on your code. After you have updated the form, ensure that the submit action follows the next example. - - - - - - + + + + ```tsx import { signUp } from "supertokens-web-js/recipe/emailpassword"; async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { - let response = await signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - },{ - id: "name", - value: name - }, { - id: "age", - value: age + "" - }, { - id: "country", - value: country - }] - }) - // ... rest of the code + let response = await signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + { + id: "name", + value: name, + }, + { + id: "age", + value: age + "", + }, + { + id: "country", + value: country, + }, + ], + }); + // ... rest of the code } ``` - - - - -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function signUpClicked(email: string, password: string, name: string, age: number, country: string) { - let response = await supertokensEmailPassword.signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }, { - id: "name", - value: name - }, { - id: "age", - value: age + "" - }, { - id: "country", - value: country - }] - }) - // ... rest of the code + let response = await supertokensEmailPassword.signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + { + id: "name", + value: name, + }, + { + id: "age", + value: age + "", + }, + { + id: "country", + value: country, + }, + ], + }); + // ... rest of the code } ``` - - - - - - - - - - - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signup' +curl --location --request POST '/auth/signup' --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "formFields": [{ @@ -126,102 +120,100 @@ curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signu }] }' ``` + + - - - - - + + - - -```tsx preview="/img/emailpassword/signup-with-name-and-age.png" previewAlt="Prebuilt form UI with extra custom fields" + + +```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - // highlight-start - signUpForm: { - formFields: [{ - id: "name", - label: "Full name", - placeholder: "First name and last name" - }, { - id: "age", - label: "Your age", - placeholder: "How old are you?", - }, { - id: "country", - label: "Your country", - placeholder: "Where do you live?", - optional: true - }] - } - // highlight-end - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "name", + label: "Full name", + placeholder: "First name and last name", + }, + { + id: "age", + label: "Your age", + placeholder: "How old are you?", + }, + { + id: "country", + label: "Your country", + placeholder: "Where do you live?", + optional: true, + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signup-with-name-and-age.png" previewAlt="Prebuilt form UI with extra custom fields" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - // highlight-start - signUpForm: { - formFields: [{ - id: "name", - label: "Full name", - placeholder: "First name and last name" - }, { - id: "age", - label: "Your age", - placeholder: "How old are you?", - }, { - id: "country", - label: "Your country", - placeholder: "Where do you live?", - optional: true - }] - } - // highlight-end - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "name", + label: "Full name", + placeholder: "First name and last name", + }, + { + id: "age", + label: "Your age", + placeholder: "How old are you?", + }, + { + id: "country", + label: "Your country", + placeholder: "Where do you live?", + optional: true, + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + #### Create custom components @@ -229,119 +221,121 @@ supertokensUIInit({ By default, the new fields use `input` elements. To enable more complex fields you can create your own custom components. -:::important -You may need to disable the Shadow DOM if you're integrating with a different component library that requires you to import its own CSS. For instance, some component libraries, such as [react-international-phone](https://github.com/goveo/react-international-phone), might expect you to include their CSS alongside their components. For more information, refer to [Disable use of shadow DOM](/docs/references/frontend-sdks/prebuilt-ui/shadow-dom). +:::note +You may need to disable the Shadow DOM if you're integrating with a different component library that requires you to import its own CSS. For instance, some component libraries, such as [react-international-phone](https://github.com/goveo/react-international-phone), might expect you to include their CSS alongside their components. For more information, refer to [Disable use of shadow DOM](/references/frontend-sdks/prebuilt-ui/shadow-dom). ::: Set the `inputComponent` property for each field that you want to customize. - - + + +:::warning[This is not applicable for non React apps. You have to create your own custom UI instead.] +::: + + -```tsx preview="/img/emailpassword/signup-with-custom-components.png" previewAlt="Pre-built form UI with custom components" + + +```tsx import SuperTokens from "supertokens-auth-react"; -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - // highlight-start - signUpForm: { - formFields: [{ - id: "select-dropdown", - label: "Select Dropdown", - inputComponent: ({ value, name, onChange }) => ( -
-
- -
-
- ), - optional: true, - }, - { - id: "terms", - label: "", - optional: false, - nonOptionalErrorMsg: "You must accept the terms and conditions", - inputComponent: ({ name, onChange }) => ( -
- onChange(e.target.checked.toString())}> - - I agree to the{" "} - - Terms and Conditions - - -
- ), - }] - } - // highlight-end - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "select-dropdown", + label: "Select Dropdown", + inputComponent: ({ value, name, onChange }) => ( +
+
+ +
+
+ ), + optional: true, + }, + { + id: "terms", + label: "", + optional: false, + nonOptionalErrorMsg: "You must accept the terms and conditions", + inputComponent: ({ name, onChange }) => ( +
+ onChange(e.target.checked.toString())}> + + I agree to the{" "} + + Terms and Conditions + + +
+ ), + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` -
- - + + -:::caution -This is not applicable for non React apps. You have to create your own custom UI instead. -::: - - -
+ + -
+ ### 2. Include the extra fields in the backend configuration Change the **Backend SDK** initialization call to ensure that the system processes the new fields when a new user registers. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; @@ -351,7 +345,7 @@ SuperTokens.init({ appInfo: { apiDomain: "...", appName: "...", - websiteDomain: "..." + websiteDomain: "...", }, supertokens: { connectionURI: "...", @@ -359,26 +353,28 @@ SuperTokens.init({ recipeList: [ EmailPassword.init({ signUpFeature: { - // highlight-start - formFields: [{ - id: "name" - }, { - id: "age" - }, { - id: "country", - optional: true - }] - // highlight-end - } + formFields: [ + { + id: "name", + }, + { + id: "age", + }, + { + id: "country", + optional: true, + }, + ], + }, + }), + Session.init({ + /* ... */ }), - Session.init({ /* ... */ }) - ] + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -392,7 +388,6 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ - //highlight-start FormFields: []epmodels.TypeInputFormField{ { ID: "name", @@ -405,102 +400,93 @@ func main() { Optional: &countryOptional, }, }, - //highlight-end }, }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword import InputFormField init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - - framework='...', # type: ignore + + framework='...', recipe_list=[ emailpassword.init( - # highlight-start sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[InputFormField(id='name'), InputFormField(id='age'), InputFormField(id='country', optional=True)] ) - # highlight-end ), session.init() ] ) ``` - - - + + ### 3. Save the values after a successful sign up Use the `signUpPOST` API function to process the field values and persist them. -:::caution -**SuperTokens** does not store custom form fields. You can either save them in your database or use the [User Metadata feature ](/docs/post-authentication/user-management/user-metadata). +:::warning +**SuperTokens** does not store custom form fields. You can either save them in your database or use the [User Metadata feature ](/post-authentication/user-management/user-metadata). ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signUpPOST: async function (input) { - - if (originalImplementation.signUpPOST === undefined) { - throw Error("Should never come here"); - } - - // First we call the original implementation of signUpPOST. - let response = await originalImplementation.signUpPOST(input); - - // Post sign up response, we check if it was successful - if (response.status === "OK") { - - // These are the input form fields values that the user used while signing up - let formFields = input.formFields; - - } - return response; - } - } - } - } - }), - Session.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + if (originalImplementation.signUpPOST === undefined) { + throw Error("Should never come here"); + } + + // First we call the original implementation of signUpPOST. + let response = await originalImplementation.signUpPOST(input); + + // Post sign up response, we check if it was successful + if (response.status === "OK") { + // These are the input form fields values that the user used while signing up + let formFields = input.formFields; + } + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -541,10 +527,9 @@ func main() { }) } ``` - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.interfaces import ( @@ -557,7 +542,6 @@ from typing import List, Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_email_password_apis(original_implementation: APIInterface): original_sign_up_post = original_implementation.sign_up_post @@ -590,113 +574,102 @@ def override_email_password_apis(original_implementation: APIInterface): return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( - # highlight-start override=emailpassword.InputOverrideConfig( apis=override_email_password_apis ) - # highlight-end ), session.init(), ], ) ``` - - - + + --- ## Customize each form field - + -:::caution Not applicable +:::warning[Not applicable] This section is not relevant for custom UI, as you create your own UI and already have control over the form fields. ::: - + - + ### Modify labels and placeholders To change the labels and placeholders of the fields, update the `formFields` property, in the recipe configuration. - - - -```tsx preview="/img/emailpassword/custom-field-name-signup-ep.png" previewAlt="Prebuilt form UI with custom labels and placeholder" + + +```tsx import SuperTokens from "supertokens-auth-react"; -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - // highlight-start - formFields: [{ - id: "email", - label: "customFieldName", - placeholder: "Custom value" - }] - } - // highlight-end - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "customFieldName", + placeholder: "Custom value", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/custom-field-name-signup-ep.png" previewAlt="Prebuilt form UI with custom labels and placeholder" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - // highlight-start - formFields: [{ - id: "email", - label: "customFieldName", - placeholder: "Custom value" - }] - } - // highlight-end - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "customFieldName", + placeholder: "Custom value", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + ### Set default values @@ -704,88 +677,78 @@ supertokensUIInit({ Add a `getDefaultValue` option to the `formFields` configuration to set default values. Keep in mind that the function needs to return a string. - - - - -```tsx preview="/img/emailpassword/signup-with-default-values.png" previewAlt="Prebuilt form UI with default values for fields" + + +```tsx import SuperTokens from "supertokens-auth-react"; -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "email", - label: "Your Email", - // highlight-start - getDefaultValue: () => "john.doe@gmail.com" - // highlight-end - }, { - id: "name", - label: "Full name", - // highlight-start - getDefaultValue: () => "John Doe", - // highlight-end - }] - } - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "Your Email", + getDefaultValue: () => "john.doe@gmail.com", + }, + { + id: "name", + label: "Full name", + getDefaultValue: () => "John Doe", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signup-with-default-values.png" previewAlt="Prebuilt form UI with default values for fields" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "email", - label: "Your Email", - // highlight-start - getDefaultValue: () => "john.doe@gmail.com" - // highlight-end - }, { - id: "name", - label: "Full name", - // highlight-start - getDefaultValue: () => "John Doe", - // highlight-end - }] - } - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "Your Email", + getDefaultValue: () => "john.doe@gmail.com", + }, + { + id: "name", + label: "Full name", + getDefaultValue: () => "John Doe", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - - + + ### Change the optional error message @@ -793,90 +756,82 @@ supertokensUIInit({ When you try to submit the login form without filling in the required fields, the UI, by default, shows an error stating that the `Field is not optional`. To customize this message set the `nonOptionalErrorMsg` property to a custom string. - - - -```tsx preview="/img/emailpassword/signup-with-custom-error-msg.png" previewAlt="Prebuilt form UI with custom error message" + + +```tsx import SuperTokens from "supertokens-auth-react"; -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "email", - label: "Your Email", - placeholder: "Email", - // highlight-start - nonOptionalErrorMsg: "Please add your email" - // highlight-end - }, { - id: "name", - label: "Full name", - placeholder: "Name", - // highlight-start - nonOptionalErrorMsg: "Full name is required", - // highlight-end - }] - } - } - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "Your Email", + placeholder: "Email", + nonOptionalErrorMsg: "Please add your email", + }, + { + id: "name", + label: "Full name", + placeholder: "Name", + nonOptionalErrorMsg: "Full name is required", + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signup-with-custom-error-msg.png" previewAlt="Prebuilt form UI with custom error message" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword" -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "email", - label: "Your Email", - placeholder: "Email", - // highlight-start - nonOptionalErrorMsg: "Please add your email" - // highlight-end - }, { - id: "name", - label: "Full name", - placeholder: "Name", - // highlight-start - nonOptionalErrorMsg: "Full name is required", - // highlight-end - }] - } - } - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "Your Email", + placeholder: "Email", + nonOptionalErrorMsg: "Please add your email", + }, + { + id: "name", + label: "Full name", + placeholder: "Name", + nonOptionalErrorMsg: "Full name is required", + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + ### Change the field order @@ -884,294 +839,273 @@ supertokensUIInit({ To customize the order of fields in your sign up form, override the `EmailPasswordSignUpForm` component. Use the next example as a reference. - - + + +:::warning[This is not applicable for non React apps. You have to create your own custom UI instead.] - - - +::: + + -```tsx preview="/img/emailpassword/signup-with-custom-field-order.png" previewAlt="Pre-built form UI with custom fields order" + + + + +```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; function App() { - return ( - - { - return ( - id === 'name')!, - props.formFields.find(({ id }) => id === 'email')!, - props.formFields.find(({ id }) => id === 'password')!, - ]} - /> - ); - }, - // highlight-end - }}> - {/* Rest of the JSX */} - - - ); + return ( + + { + return ( + id === "name")!, + props.formFields.find(({ id }) => id === "email")!, + props.formFields.find(({ id }) => id === "password")!, + ]} + /> + ); + }, + }} + > + {/* Rest of the JSX */} + + + ); } export default App; ``` - - - - -```tsx preview="/img/emailpassword/signup-with-custom-field-order.png" previewAlt="Pre-built form UI with custom fields order" + + +```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; -import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui" +import { getRoutingComponent, canHandleRoute } from "supertokens-auth-react/ui"; function App() { - if (canHandleRoute([EmailPasswordPreBuiltUI])) { - return ( - { - return ( - id === 'name')!, - props.formFields.find(({ id }) => id === 'email')!, - props.formFields.find(({ id }) => id === 'password')!, - ]} - /> - ); - }, - // highlight-end - }}> - {getRoutingComponent([EmailPasswordPreBuiltUI])} - - ) - } + if (canHandleRoute([EmailPasswordPreBuiltUI])) { return ( - - {/* Rest of the JSX */} - + { + return ( + id === "name")!, + props.formFields.find(({ id }) => id === "email")!, + props.formFields.find(({ id }) => id === "password")!, + ]} + /> + ); + }, + }} + > + {getRoutingComponent([EmailPasswordPreBuiltUI])} + ); + } + return {/* Rest of the JSX */}; } export default App; ``` + + + + - - - + + - - - -:::caution - -This is not applicable for non React apps. You have to create your own custom UI instead. - -::: - - - - - - - - - + --- -## Change field validators +## Change field validators ### 1. Update the frontend configuration - + -:::caution Not applicable +:::warning[Not applicable] For your custom UI, you have to implement field validation checking yourself. Note that you need to also update the backend validation to ensure a complete flow. Check the next section for more details. ::: - + - + Add a `validate` method to any of your `formFields`. The following example shows how to add age verification to the form: - - - -```tsx preview="/img/emailpassword/signup-with-name-and-age-failure.png" previewAlt="Pre-built form UI with custom validation" + + +```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "name", - label: "Full name", - placeholder: "First name and last name" - }, { - id: "age", - label: "Your age", - placeholder: "How old are you?", - optional: true, - - /* Validation method to make sure that age is above 18 */ - // highlight-start - validate: async (value) => { - if (parseInt(value) > 18) { - return undefined; // means that there is no error - } - return "You must be over 18 to register"; - } - // highlight-end - - }, { - id: "country", - label: "Your country", - placeholder: "Where do you live?", - optional: true - }] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "name", + label: "Full name", + placeholder: "First name and last name", + }, + { + id: "age", + label: "Your age", + placeholder: "How old are you?", + optional: true, + + /* Validation method to make sure that age is above 18 */ + validate: async (value) => { + if (parseInt(value) > 18) { + return undefined; // means that there is no error } - } - }), - Session.init() - ] + return "You must be over 18 to register"; + }, + }, + { + id: "country", + label: "Your country", + placeholder: "Where do you live?", + optional: true, + }, + ], + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx preview="/img/emailpassword/signup-with-name-and-age-failure.png" previewAlt="Pre-built form UI with custom validation" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - id: "name", - label: "Full name", - placeholder: "First name and last name" - }, { - id: "age", - label: "Your age", - placeholder: "How old are you?", - optional: true, - - /* Validation method to make sure that age is above 18 */ - // highlight-start - validate: async (value) => { - if (parseInt(value) > 18) { - return undefined; // means that there is no error - } - return "You must be over 18 to register"; - } - // highlight-end - - }, { - id: "country", - label: "Your country", - placeholder: "Where do you live?", - optional: true - }] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "name", + label: "Full name", + placeholder: "First name and last name", + }, + { + id: "age", + label: "Your age", + placeholder: "How old are you?", + optional: true, + + /* Validation method to make sure that age is above 18 */ + validate: async (value) => { + if (parseInt(value) > 18) { + return undefined; // means that there is no error } - } - }), - supertokensUISession.init() - ] + return "You must be over 18 to register"; + }, + }, + { + id: "country", + label: "Your country", + placeholder: "Where do you live?", + optional: true, + }, + ], + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + - + ### 2. Update the backend configuration -Add `validate` functions to each of the form fields, in the backend SDK initialization call. - - - +Add `validate` functions to each of the form fields, in the backend SDK initialization call. + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [{ - id: "name" - }, { - id: "age", - /* Validation method to make sure that age >= 18 */ - // highlight-start - validate: async (value, tenantId) => { - if (parseInt(value) >= 18) { - return undefined; // means that there is no error - } - return "You must be over 18 to register"; - } - // highlight-end - }, { - id: "country", - optional: true - }] - } - }), - Session.init({ - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: "name", + }, + { + id: "age", + /* Validation method to make sure that age >= 18 */ + validate: async (value, tenantId) => { + if (parseInt(value) >= 18) { + return undefined; // means that there is no error + } + return "You must be over 18 to register"; + }, + }, + { + id: "country", + optional: true, + }, + ], + }, + }), + Session.init({}), + ], }); ``` - - - + + ```go import ( "strconv" @@ -1193,7 +1127,6 @@ func main() { }, { ID: "age", - //highlight-start Validate: func(value interface{}, tenantId string) *string { age, _ := strconv.Atoi(value.(string)) if age >= 18 { @@ -1203,7 +1136,6 @@ func main() { err := "You must be over 18 to register" return &err }, - //highlight-end }, { ID: "country", @@ -1216,36 +1148,30 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword import InputFormField from typing import Any -# highlight-start async def validate_age(value: Any, tenant_id: str): # Validation method to make sure that age >= 18 if int(value) >= 18: return None # means that there is no error return 'You must be over 18 to register' -# highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - - framework='...', # type: ignore + + framework='...', recipe_list=[ emailpassword.init( sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[ InputFormField(id='name'), - # highlight-start InputFormField(id='age', validate=validate_age), - # highlight-end InputFormField(id='country', optional=True) ] ) @@ -1253,11 +1179,10 @@ init( ] ) ``` + + - - - -:::info Multi-tenancy +:::info[Multi-tenancy] Notice the `tenantId` argument passed into the `validate` function. Using that, you can define custom logic per tenant. For example, you can define different password policies for different tenants. @@ -1274,8 +1199,7 @@ By default, SuperTokens adds an email and a password validator to the sign up fo To add your own validators follow the steps described initially in this section. -:::important -- The email validator that you define for **sign up** is also applied automatically to **sign in**. +:::note[- The email validator that you define for **sign up** is also applied automatically to **sign in**.] - The password validator that you define for **sign up** is also applied automatically to **reset password** forms. ::: @@ -1284,147 +1208,143 @@ Here is an example of what you need to change. ##### 1. Update the frontend configuration - + -:::caution Not applicable +:::warning[Not applicable] For your custom UI, you have to implement field validation checking yourself. Note that you need to also update the backend validation to ensure a complete flow. Check the next section for more details. ::: - - - - - + + + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - // highlight-start - id: "email", - label: "...", - validate: async (value) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - }, { - id: "password", - label: "...", - validate: async (value) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - // highlight-end - }] - } - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "...", + validate: async (value) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + { + id: "password", + label: "...", + validate: async (value) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + ], + }, + }, + }), + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - signInAndUpFeature: { - signUpForm: { - formFields: [{ - // highlight-start - id: "email", - label: "...", - validate: async (value) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - }, { - id: "password", - label: "...", - validate: async (value) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - // highlight-end - }] - } - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + signInAndUpFeature: { + signUpForm: { + formFields: [ + { + id: "email", + label: "...", + validate: async (value) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + { + id: "password", + label: "...", + validate: async (value) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + ], + }, + }, + }), + ], }); ``` - - - + + + ##### 1. Update the backend configuration - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [{ - // highlight-start - id: "email", - validate: async (value, tenantId) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - }, { - id: "password", - validate: async (value, tenantId) => { - // Your own validation returning a string or undefined if no errors. - return "..."; - } - // highlight-end - }] - } - }), - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: "email", + validate: async (value, tenantId) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + { + id: "password", + validate: async (value, tenantId) => { + // Your own validation returning a string or undefined if no errors. + return "..."; + }, + }, + ], + }, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -1438,7 +1358,6 @@ func main() { emailpassword.Init(&epmodels.TypeInput{ SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ - //highlight-start { ID: "email", Validate: func(value interface{}, tenantId string) *string { @@ -1453,7 +1372,6 @@ func main() { return nil }, }, - //highlight-end }, }, }), @@ -1461,52 +1379,43 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword import InputFormField from typing import Any -# highlight-start async def validate_password(value: Any, tenant_id: str): pass # TODO -# highlight-end -# highlight-start async def validate_email(value: Any, tenant_id: str): pass # TODO -# highlight-end init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( sign_up_feature=emailpassword.InputSignUpFeature( form_fields=[ - # highlight-start InputFormField(id='password', validate=validate_password), InputFormField(id='email', validate=validate_email) - # highlight-end ] ) ) ] ) ``` - - - + + --- ## Add terms of service and privacy policy links - + To add "Terms of service" and "Privacy policy" links to your sign up page add the links in the frontend SDK initialization call. Based on the provided configuration the data renders in the following way: @@ -1514,67 +1423,61 @@ Based on the provided configuration the data renders in the following way: - Provided only Terms of service link: "By signing up, you agree to the [Terms of Service](#add-terms-of-service-and-privacy-policy-links)" - Provided only Privacy policy link: "By signing up, you agree to the [Privacy Policy](#add-terms-of-service-and-privacy-policy-links)" - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - termsOfServiceLink: "https://example.com/terms-of-service", - privacyPolicyLink: "https://example.com/privacy-policy", - // highlight-end - recipeList: [/* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + termsOfServiceLink: "https://example.com/terms-of-service", + privacyPolicyLink: "https://example.com/privacy-policy", + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - termsOfServiceLink: "https://example.com/terms-of-service", - privacyPolicyLink: "https://example.com/privacy-policy", - // highlight-end - recipeList: [/* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + termsOfServiceLink: "https://example.com/terms-of-service", + privacyPolicyLink: "https://example.com/privacy-policy", + recipeList: [ + /* ... */ + ], }); ``` + + - - - - - + + -:::caution -Not applicable since you do not use the pre-built UI. +:::warning[Not applicable since you do not use the pre-built UI.] ::: - + --- ## See also - - - - - - + + + + + + diff --git a/docs/authentication/email-password/disable-signup.mdx b/docs/authentication/email-password/disable-signup.mdx index 19f6830a54..966c753887 100644 --- a/docs/authentication/email-password/disable-signup.mdx +++ b/docs/authentication/email-password/disable-signup.mdx @@ -1,21 +1,14 @@ --- title: Disable Sign Up -hide_title: true description: Learn how to disable the sign up flow -sidebar_position: 8 -page_type: guide -recipe: emailpassword -category: email-password +sidebar: + order: 80 --- +Learn how to disable the sign up flow for the `EmailPassword` recipe. -# Disable the Sign Up Flow - - - Learn how to disable the sign up flow for the `EmailPassword` recipe. - +--- - ## Overview @@ -28,126 +21,119 @@ This can be done in two steps: ## Before you start This guide assumes that you already have configured your application to use **SuperTokens** for authentication. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). - +If you have not, please check the [Quickstart Guide](/quickstart). ## Remove the sign up UI - - - - - + - + - + + Remove the sign up UI by overriding the `AuthPageComponentList` component and setting the `showSuperTokensAuth` prop to `false`. - + + +Remove the sign up UI by customizing the `CSS` of the authentication page. + + + + + ```tsx import React from "react"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { AuthRecipeComponentsOverrideContextProvider } from "supertokens-auth-react/ui"; -import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword" +import { EmailPasswordComponentsOverrideProvider } from "supertokens-auth-react/recipe/emailpassword"; import { ThirdpartyComponentsOverrideProvider } from "supertokens-auth-react/recipe/thirdparty"; function App() { - return ( - - { - return ; - }, - }}> - - - ); + return ( + + { + return ; + }, + }} + > + + ); } export default App; ``` - - - - - -Remove the sign up UI by customizing the `CSS` of the authentication page. - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=authPage] [data-supertokens~=headerSubtitle] { display: none; } `, - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` + + - - - - - + - + If you have a custom UI, this step will depend on your implementation. Just make sure that the user will not be able to view any sign up elements on the authentication page. - + ## Disable the Backend SDK sign up endpoints Override the **Backend SDK** API functions to prevent sign up attempts. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - // highlight-next-line - signUpPOST: undefined, - } - } - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: undefined, + }; + }, + }, + }), + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -161,7 +147,6 @@ func main() { emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { - // highlight-next-line originalImplementation.SignUpPOST = nil return originalImplementation }, @@ -171,22 +156,20 @@ func main() { }) } ``` - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import APIInterface def apis_override(original_impl: APIInterface): - # highlight-next-line original_impl.disable_sign_up_post = True return original_impl init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( @@ -196,26 +179,21 @@ init( ] ) ``` - - - + + ## See also - - - Account Creation - Import accounts using the SuperTokens API. - - - User Management - SDK functions that can be used to manage users. - - - Dashboard - UI exposed by the SuperTokens SDK that allows you to view and manage users. - - - + + +Import accounts using the SuperTokens API. + + +SDK functions that can be used to manage users. + + +UI exposed by the SuperTokens SDK that allows you to view and manage users. + + diff --git a/docs/authentication/email-password/hooks-and-overrides.mdx b/docs/authentication/email-password/hooks-and-overrides.mdx index 4129a8ea75..dca67cd6c4 100644 --- a/docs/authentication/email-password/hooks-and-overrides.mdx +++ b/docs/authentication/email-password/hooks-and-overrides.mdx @@ -1,192 +1,161 @@ --- title: Hooks and overrides -hide_title: true -sidebar_position: 4 -description: >- - Add custom logic in the authentication flow by overriding the SuperTokens - APIs. -page_type: guide -recipe: emailpassword -category: email-password +description: Add custom logic in the authentication flow by overriding the SuperTokens APIs. +sidebar: + order: 40 --- -# Hooks and overrides - - -**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. +**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can modify adjust the `emailpassword` recipe to your needs. -Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. +Explore the [references pages](/references) for a more in depth guide on hooks and overrides. -## Sign in +## Sign in -### Frontend hook +### Frontend hook - + - + This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. -You can use it to send analytics events. - - - - - - +You can use it to send analytics events. + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + Session.init(), + ], }); ``` - - - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + supertokensUISession.init(), + ], }); ``` + + - - - - - - + - + -:::caution Not applicable +:::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do post sign in based on the result of the API call. ::: - + -### Backend override +### Backend override Overriding the `signIn` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signIn: async function (input) { - - // First we call the original implementation of signIn. - let response = await originalImplementation.signIn(input); - - // Post sign up response, we check if it was successful - if (response.status === "OK") { - /** - * - * response.user contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ - - // TODO: post sign in logic - } - return response; - } - } - } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signIn: async function (input) { + // First we call the original implementation of signIn. + let response = await originalImplementation.signIn(input); + + // Post sign up response, we check if it was successful + if (response.status === "OK") { + /** + * + * response.user contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + // TODO: post sign in logic + } + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "fmt" @@ -200,7 +169,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - //highlight-start Override: &epmodels.OverrideStruct{ Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // create a copy of the originalImplementation func @@ -231,17 +199,14 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( @@ -252,7 +217,6 @@ from typing import Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_emailpassword_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: @@ -290,134 +254,111 @@ def override_emailpassword_functions( return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( - # highlight-start override=emailpassword.InputOverrideConfig( functions=override_emailpassword_functions ), - # highlight-end ), session.init(), ], ) ``` - - - + + --- ## Sign up -### Frontend hook +### Frontend hook This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up. - - - - - + + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + Session.init(), + ], }); ``` - - - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + supertokensUISession.init(), + ], }); ``` + + - - - - - - + - + -:::caution Not applicable +:::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign up API yourself anyway. You can perform anything you want to do post sign up based on the result of the API call. ::: - + -### Backend override +### Backend override Overriding the `signUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data, synchronize users between **SuperTokens** and your systems or to trigger other types of actions. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; @@ -425,56 +366,55 @@ import Session from "supertokens-node/recipe/session"; // backend SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signUp: async function (input) { - // First we call the original implementation of signUp. - let response = await originalImplementation.signUp(input); - - // Post sign up response, we check if it was successful - if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { - /** - * - * response.user contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ - // TODO: post sign up logic - } - return response; - } - } - } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signUp: async function (input) { + // First we call the original implementation of signUp. + let response = await originalImplementation.signUp(input); + + // Post sign up response, we check if it was successful + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { + /** + * + * response.user contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + // TODO: post sign up logic + } + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "fmt" @@ -489,7 +429,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - //highlight-start Override: &epmodels.OverrideStruct{ Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { // create a copy of the originalImplementation func @@ -520,17 +459,14 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( @@ -541,7 +477,6 @@ from typing import Dict, Any, Union from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_emailpassword_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: @@ -579,26 +514,21 @@ def override_emailpassword_functions( return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( - # highlight-start override=emailpassword.InputOverrideConfig( functions=override_emailpassword_functions ), - # highlight-end ), session.init(), ], ) ``` - - - + + --- @@ -606,97 +536,78 @@ init( ## Password reset -### Frontend hook +### Frontend hook - + This method gets fired during the password reset flow with either the `PASSWORD_RESET_SUCCESSFUL` or `RESET_PASSWORD_EMAIL_SENT` action. Use it to fire analytics events or to add any additional logic. - - - - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "PASSWORD_RESET_SUCCESSFUL") { - // Add you custom logic here - } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { - // Add you custom logic here - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "PASSWORD_RESET_SUCCESSFUL") { + // Add you custom logic here + } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { + // Add you custom logic here + } + }, + }), + Session.init(), + ], }); ``` - - - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "PASSWORD_RESET_SUCCESSFUL") { - // Add you custom logic here - } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { - // Add you custom logic here - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + onHandleEvent: async (context) => { + if (context.action === "PASSWORD_RESET_SUCCESSFUL") { + // Add you custom logic here + } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { + // Add you custom logic here + } + }, + }), + supertokensUISession.init(), + ], }); ``` + + + - - - - + - - - -:::caution Not applicable +:::warning[Not applicable] This section is not applicable for custom UI since you are calling the sign in API yourself anyway. You can perform anything you want to do during the password reset flow based on the result of the API call. ::: - + ### Backend override @@ -704,54 +615,49 @@ This section is not applicable for custom UI since you are calling the sign in A Overriding the `passwordResetPOST` function allows you to introduce your own logic for the password reset process. Use it to introduce your own logic for the flow. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - passwordResetPOST: async function(input) { - - if (originalImplementation.passwordResetPOST === undefined) { - throw Error("Should never come here"); - } - - // First we call the original implementation - let response = await originalImplementation.passwordResetPOST(input); - - // Then we check if it was successfully completed - if (response.status === "OK") { - // TODO: post password reset logic - } - return response; - } - }; - }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + passwordResetPOST: async function (input) { + if (originalImplementation.passwordResetPOST === undefined) { + throw Error("Should never come here"); + } + + // First we call the original implementation + let response = await originalImplementation.passwordResetPOST(input); + + // Then we check if it was successfully completed + if (response.status === "OK") { + // TODO: post password reset logic + } + return response; }, - // highlight-end - }), - Session.init() - ] + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -764,7 +670,6 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - //highlight-start Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { @@ -791,17 +696,15 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( @@ -813,7 +716,6 @@ from supertokens_python.recipe.emailpassword.types import FormField from typing import Dict, List, Any -# highlight-start def override_apis(original_implementation: APIInterface): original_password_reset_post = original_implementation.password_reset_post @@ -838,33 +740,28 @@ def override_apis(original_implementation: APIInterface): return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( - # highlight-start override=emailpassword.InputOverrideConfig(apis=override_apis) - # highlight-end ) ], ) ``` - - - + + --- ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/email-password/implement-username-login.mdx b/docs/authentication/email-password/implement-username-login.mdx index b3111b6d52..77990c1ea0 100644 --- a/docs/authentication/email-password/implement-username-login.mdx +++ b/docs/authentication/email-password/implement-username-login.mdx @@ -1,24 +1,14 @@ --- title: Implement username login -hide_title: true -sidebar_position: 7 -toc_max_heading_level: 4 -description: >- - Enable username login by replacing email with username in the SuperTokens - authentication flow. -page_type: guide -recipe: emailpassword -category: email-password +description: Enable username login by replacing email with username in the SuperTokens authentication flow. +sidebar: + order: 60 --- - -# Implement username login - - ## Overview -This tutorial shows you how to customize the recipe to add username based login with an optional email field. +This tutorial shows you how to customize the recipe to add username based login with an optional email field. A few variations exist on how username-based flows can work: @@ -38,8 +28,8 @@ Then the optional email value gets saved against the `userID` of the user and yo ## Before you start -This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +This guide assumes that you have already implemented the [EmailPassword recipe](/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. +If you have not, please check the [Quickstart Guide](/quickstart). ## Steps @@ -52,62 +42,61 @@ Hence it needs to also match an email format since the user might enter it when Inside **SuperTokens**, the field is still called `email`. This ensures that the username is unique and that the authentication flows works. -Use the next code snippet as a reference. +Use the next code snippet as a reference. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [{ - id: "email", - validate: async (value) => { - if (typeof value !== "string") { - return "Please provide a string input." - } - - // first we check for if it's an email - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) !== null - ) { - return undefined; - } - - // since it's not an email, we check for if it's a correct username - if (value.length < 3) { - return "Usernames must be at least 3 characters long." - } - if (!value.match(/^[a-z0-9_-]+$/)) { - return "Username must contain only alphanumeric, underscore or hyphen characters." - } - } - }] - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: "email", + validate: async (value) => { + if (typeof value !== "string") { + return "Please provide a string input."; + } + + // first we check for if it's an email + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) !== null + ) { + return undefined; + } + + // since it's not an email, we check for if it's a correct username + if (value.length < 3) { + return "Usernames must be at least 3 characters long."; + } + if (!value.match(/^[a-z0-9_-]+$/)) { + return "Username must contain only alphanumeric, underscore or hyphen characters."; + } + }, + }, + ], + }, + }), + ], }); ``` - - - - + + ```go import ( "regexp" @@ -157,11 +146,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from supertokens_python import InputAppInfo, init @@ -190,7 +177,7 @@ async def validate(value: str, tenant_id: str): init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ @@ -200,9 +187,8 @@ init( ] ) ``` - - - + + ### 2. Save the user email @@ -211,77 +197,75 @@ init( The sign up `API` takes in the username, password, and an optional email. Add a new form field for the email, along with a `validate` function that checks the uniqueness and syntax of the input email. -:::warning Custom Implementation +:::warning[Custom Implementation] To check if the email is unique you need to persist values in your own database and then check against them. ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; -let emailUserMap: {[key: string]: string} = {} +let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { - // TODO: Check your database for if the email is associated with a user - // and return that user ID if it is. + // TODO: Check your database for if the email is associated with a user + // and return that user ID if it is. - // this is just a placeholder implementation - return emailUserMap[email]; + // this is just a placeholder implementation + return emailUserMap[email]; } SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - signUpFeature: { - formFields: [{ - id: "email", - validate: async (value) => { - // ...from previous code snippet... - return undefined - } - }, { - // highlight-start - id: "actualEmail", - validate: async (value) => { - if (value === "") { - // this means that the user did not provide an email - return undefined; - } - if ( - value.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) === null - ) { - return "Email is invalid"; - } - - if ((await getUserUsingEmail(value)) !== undefined) { - return "Email already in use. Please sign in, or use another email" - } - }, - optional: true - // highlight-end - }] - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + signUpFeature: { + formFields: [ + { + id: "email", + validate: async (value) => { + // ...from previous code snippet... + return undefined; + }, + }, + { + id: "actualEmail", + validate: async (value) => { + if (value === "") { + // this means that the user did not provide an email + return undefined; + } + if ( + value.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) === null + ) { + return "Email is invalid"; + } + + if ((await getUserUsingEmail(value)) !== undefined) { + return "Email already in use. Please sign in, or use another email"; + } + }, + optional: true, + }, + ], + }, + }), + ], }); ``` - - - - + + ```go import ( "regexp" @@ -320,7 +304,6 @@ func main() { return nil }, }, - // highlight-start { ID: "actualEmail", Validate: func(value interface{}, tenantId string) *string { @@ -347,7 +330,6 @@ func main() { }, Optional: &actualEmailOptional, }, - // highlight-end }, }, }), @@ -355,11 +337,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Dict @@ -370,7 +350,6 @@ from supertokens_python.recipe.emailpassword.utils import InputSignUpFeature email_user_map: Dict[str, str] = {} -# highlight-start async def get_user_using_email(email: str): # TODO: Check your database for if the email is associated with a user # and return that user ID if it is. @@ -379,7 +358,6 @@ async def get_user_using_email(email: str): if email in email_user_map: return email_user_map[email] return None -# highlight-end async def validate(value: str, tenant_id: str): # from previous code snippet.. @@ -399,23 +377,20 @@ async def validate_actual_email(value: str, tenant_id: str): init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ InputFormField(id="email", validate=validate), - # highlight-start InputFormField(id="actualEmail", validate=validate_actual_email, optional=True) - # highlight-end ]) ) ] ) ``` - - - + + #### 2.2 Save the email field value @@ -424,76 +399,73 @@ Override the sign up API to save the custom email form field. Use a mapping of `userID` to `email` to keep track of the association. Save the email value in your own database. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; -let emailUserMap: {[key: string]: string} = {} +let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { - // TODO: Check your database for if the email is associated with a user - // and return that user ID if it is. - - // this is just a placeholder implementation - return emailUserMap[email]; + // TODO: Check your database for if the email is associated with a user + // and return that user ID if it is. + + // this is just a placeholder implementation + return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { - // TODO: Save email and userId mapping + // TODO: Save email and userId mapping - // this is just a placeholder implementation - emailUserMap[email] = userId + // this is just a placeholder implementation + emailUserMap[email] = userId; } SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - apis: (original) => { - return { - ...original, - signUpPOST: async function (input) { - let response = await original.signUpPOST!(input); - if (response.status === "OK") { - // sign up successful - let actualEmail = input.formFields.find(i => i.id === "actualEmail")!.value as string; - if (actualEmail === "") { - // User did not provide an email. - // This is possible since we set optional: true - // in the formField config - } else { - await saveEmailForUser(actualEmail, response.user.id) - } - } - return response - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (original) => { + return { + ...original, + signUpPOST: async function (input) { + let response = await original.signUpPOST!(input); + if (response.status === "OK") { + // sign up successful + let actualEmail = input.formFields.find((i) => i.id === "actualEmail")!.value as string; + if (actualEmail === "") { + // User did not provide an email. + // This is possible since we set optional: true + // in the formField config + } else { + await saveEmailForUser(actualEmail, response.user.id); } + } + return response; }, - // highlight-end - signUpFeature: { - formFields: [ /* ... from previous code snippet ... */] - } - }) - ] + }; + }, + }, + signUpFeature: { + formFields: [ + /* ... from previous code snippet ... */ + ], + }, + }), + ], }); ``` - - - - + + ```go import ( "errors" @@ -533,7 +505,6 @@ func main() { SignUpFeature: &epmodels.TypeInputSignUp{ FormFields: []epmodels.TypeInputFormField{ /* ... from previous code... */}, }, - // highlight-start Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { ogSignUpPOST := *originalImplementation.SignUpPOST @@ -570,17 +541,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init @@ -610,7 +578,6 @@ async def get_user_using_email(email: str): return None -# highlight-start async def save_email_for_user(email: str, user_id: str): # TODO: Save email and userId mapping @@ -656,11 +623,9 @@ def apis_override(original: APIInterface): return original -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( @@ -673,9 +638,8 @@ init( ], ) ``` - - - + + ### 3. Allow username or email during sign in @@ -686,93 +650,92 @@ Override the sign in recipe function to allow this. Use the next code snippet as a reference. The example use the `email` to `userId` mapping, mentioned earlier, to figure out which username to use. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; -let emailUserMap: { [key: string]: string } = {} +let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { - // TODO: Check your database for if the email is associated with a user - // and return that user ID if it is. + // TODO: Check your database for if the email is associated with a user + // and return that user ID if it is. - // this is just a placeholder implementation - return emailUserMap[email]; + // this is just a placeholder implementation + return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { - // TODO: Save email and userId mapping + // TODO: Save email and userId mapping - // this is just a placeholder implementation - emailUserMap[email] = userId + // this is just a placeholder implementation + emailUserMap[email] = userId; } -// highlight-start function isInputEmail(input: string): boolean { - return input.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) !== null; + return ( + input.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) !== null + ); } -// highlight-end SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - override: { - // highlight-start - functions: (original) => { - return { - ...original, - signIn: async function (input) { - if (isInputEmail(input.email)) { - let userId = await getUserUsingEmail(input.email); - if (userId !== undefined) { - let superTokensUser = await SuperTokens.getUser(userId); - if (superTokensUser !== undefined) { - // we find the right login method for this user - // based on the user ID. - let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); - - if (loginMethod !== undefined) { - input.email = loginMethod.email! - } - } - } - } - return original.signIn(input); - } - } - }, - // highlight-end - apis: (original) => { - return { - ...original, - // override from previous code snippet + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (original) => { + return { + ...original, + signIn: async function (input) { + if (isInputEmail(input.email)) { + let userId = await getUserUsingEmail(input.email); + if (userId !== undefined) { + let superTokensUser = await SuperTokens.getUser(userId); + if (superTokensUser !== undefined) { + // we find the right login method for this user + // based on the user ID. + let loginMethod = superTokensUser.loginMethods.find( + (lM) => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword", + ); + + if (loginMethod !== undefined) { + input.email = loginMethod.email!; } + } } + } + return original.signIn(input); }, - signUpFeature: { - formFields: [ /* ... from previous code snippet ... */] - } - }) - ] + }; + }, + apis: (original) => { + return { + ...original, + // override from previous code snippet + }; + }, + }, + signUpFeature: { + formFields: [ + /* ... from previous code snippet ... */ + ], + }, + }), + ], }); ``` - - - - + + ```go import ( "regexp" @@ -804,7 +767,6 @@ func saveEmailForUser(email string, userId string) error { return nil } -// highlight-start func isInputEmail(email string) bool { emailCheck, err := regexp.Match(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, []byte(email)) if err != nil || !emailCheck { @@ -812,7 +774,6 @@ func isInputEmail(email string) bool { } return true } -// highlight-end func main() { @@ -827,7 +788,6 @@ func main() { // ...from previous code snippet... return originalImplementation }, - // highlight-start Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface { ogSignIn := *originalImplementation.SignIn (*originalImplementation.SignIn) = func(email, password, tenantId string, userContext supertokens.UserContext) (epmodels.SignInResponse, error) { @@ -850,18 +810,15 @@ func main() { } return originalImplementation }, - // highlight-end }, }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict, Union @@ -895,9 +852,6 @@ async def save_email_for_user(email: str, user_id: str): email_user_map[email] = user_id -# highlight-start - - def is_input_email(email: str): return ( fullmatch( @@ -949,12 +903,9 @@ def recipe_override(original: RecipeInterface): return original -# highlight-end - - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( @@ -970,14 +921,13 @@ init( ], ) ``` - - - + + ### 4. Allow username or email during password reset The password reset flow requires the user to have added an email during sign up. -If there is no email associated with the user, return an appropriate message. +If there is no email associated with the user, return an appropriate message. To update the functionality you have to first change how the password reset token gets generated and then update the email sending logic. This way both methods take into account the new fields. @@ -988,131 +938,138 @@ The user should enter either their username or their email when starting the pas Like the sign in customization, you must check if the input is an email and, if it is, retrieve the username associated with the email. If you can't find a username from an email you have to return an appropriate message to the frontend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import supertokensTypes from "supertokens-node/types"; -let emailUserMap: { [key: string]: string } = {} +let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { - // TODO: Check your database for if the email is associated with a user - // and return that user ID if it is. + // TODO: Check your database for if the email is associated with a user + // and return that user ID if it is. - // this is just a placeholder implementation - return emailUserMap[email]; + // this is just a placeholder implementation + return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { - // TODO: Save email and userId mapping + // TODO: Save email and userId mapping - // this is just a placeholder implementation - emailUserMap[email] = userId + // this is just a placeholder implementation + emailUserMap[email] = userId; } function isInputEmail(input: string): boolean { - return input.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) !== null; + return ( + input.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) !== null + ); } -// highlight-start async function getEmailUsingUserId(userId: string) { - // TODO: check your database mapping.. - - // this is just a placeholder implementation - let emails = Object.keys(emailUserMap) - for (let i = 0; i < emails.length; i++) { - if (emailUserMap[emails[i]] === userId) { - return emails[i] - } + // TODO: check your database mapping.. + + // this is just a placeholder implementation + let emails = Object.keys(emailUserMap); + for (let i = 0; i < emails.length; i++) { + if (emailUserMap[emails[i]] === userId) { + return emails[i]; } - return undefined; + } + return undefined; } -// highlight-end SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - override: { - functions: (original) => { - return { - ...original, - // ...override from previous code snippet... - } - }, - apis: (original) => { - return { - ...original, - // ...override from previous code snippet... - // highlight-start - generatePasswordResetTokenPOST: async function (input) { - let emailOrUsername = input.formFields.find(i => i.id === "email")!.value as string; - if (isInputEmail(emailOrUsername)) { - let userId = await getUserUsingEmail(emailOrUsername); - if (userId !== undefined) { - let superTokensUser = await SuperTokens.getUser(userId); - if (superTokensUser !== undefined) { - // we find the right login method for this user - // based on the user ID. - let loginMethod = superTokensUser.loginMethods.find(lM => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword"); - if (loginMethod !== undefined) { - // we replace the input form field's array item - // to contain the username instead of the email. - input.formFields = input.formFields.filter(i => i.id !== "email") - input.formFields = [...input.formFields, { - id: "email", - value: loginMethod.email! - }] - } - } - } - } - - let username = input.formFields.find(i => i.id === "email")!.value as string; - let superTokensUsers: supertokensTypes.User[] = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: username - }); - // from the list of users that have this email, we now find the one - // that has this email with the email password login method. - let targetUser = superTokensUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(username) && lM.recipeId === "emailpassword") !== undefined); - - if (targetUser !== undefined) { - if ((await getEmailUsingUserId(targetUser.id)) === undefined) { - return { - status: "GENERAL_ERROR", - message: "You need to add an email to your account for resetting your password. Please contact support." - } - } - } - return original.generatePasswordResetTokenPOST!(input); + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (original) => { + return { + ...original, + // ...override from previous code snippet... + }; + }, + apis: (original) => { + return { + ...original, + // ...override from previous code snippet... + generatePasswordResetTokenPOST: async function (input) { + let emailOrUsername = input.formFields.find((i) => i.id === "email")!.value as string; + if (isInputEmail(emailOrUsername)) { + let userId = await getUserUsingEmail(emailOrUsername); + if (userId !== undefined) { + let superTokensUser = await SuperTokens.getUser(userId); + if (superTokensUser !== undefined) { + // we find the right login method for this user + // based on the user ID. + let loginMethod = superTokensUser.loginMethods.find( + (lM) => lM.recipeUserId.getAsString() === userId && lM.recipeId === "emailpassword", + ); + if (loginMethod !== undefined) { + // we replace the input form field's array item + // to contain the username instead of the email. + input.formFields = input.formFields.filter((i) => i.id !== "email"); + input.formFields = [ + ...input.formFields, + { + id: "email", + value: loginMethod.email!, }, - // highlight-end + ]; } + } } + } + + let username = input.formFields.find((i) => i.id === "email")!.value as string; + let superTokensUsers: supertokensTypes.User[] = await SuperTokens.listUsersByAccountInfo(input.tenantId, { + email: username, + }); + // from the list of users that have this email, we now find the one + // that has this email with the email password login method. + let targetUser = superTokensUsers.find( + (u) => + u.loginMethods.find((lM) => lM.hasSameEmailAs(username) && lM.recipeId === "emailpassword") !== + undefined, + ); + + if (targetUser !== undefined) { + if ((await getEmailUsingUserId(targetUser.id)) === undefined) { + return { + status: "GENERAL_ERROR", + message: + "You need to add an email to your account for resetting your password. Please contact support.", + }; + } + } + return original.generatePasswordResetTokenPOST!(input); }, - signUpFeature: { - formFields: [ /* ... from previous code snippet ... */] - } - }) - ] + }; + }, + }, + signUpFeature: { + formFields: [ + /* ... from previous code snippet ... */ + ], + }, + }), + ], }); ``` - - - - + + ```go import ( "regexp" @@ -1153,7 +1110,6 @@ func isInputEmail(email string) bool { return true } -// highlight-start func getEmailUsingUserId(userId string) (*string, error) { for email, mappedUserId := range emailUserMap { if mappedUserId == userId { @@ -1162,7 +1118,6 @@ func getEmailUsingUserId(userId string) (*string, error) { } return nil, nil } -// highlight-end func main() { @@ -1176,7 +1131,6 @@ func main() { APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { // ...override sign up API from previous code snippet... - // highlight-start ogGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST (*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) { @@ -1249,7 +1203,6 @@ func main() { return ogGeneratePasswordResetTokenPOST(formFields, tenantId, options, userContext) } - // highlight-end return originalImplementation }, @@ -1263,11 +1216,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict, List @@ -1316,7 +1267,6 @@ def is_input_email(email: str): ) -# highlight-start async def get_email_using_user_id(user_id: str): for email in email_user_map: if email_user_map[email] == user_id: @@ -1395,12 +1345,9 @@ def apis_override(original: APIInterface): return original -# highlight-end - - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature( @@ -1416,95 +1363,93 @@ init( ], ) ``` - - - + + #### 4.2 Override the email sending API Update the email sending API to retrieve the user email if the user used a username in the password reset flow. - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; -let emailUserMap: {[key: string]: string} = {} +let emailUserMap: { [key: string]: string } = {}; async function getUserUsingEmail(email: string): Promise { - // TODO: Check your database for if the email is associated with a user - // and return that user ID if it is. - - // this is just a placeholder implementation - return emailUserMap[email]; + // TODO: Check your database for if the email is associated with a user + // and return that user ID if it is. + + // this is just a placeholder implementation + return emailUserMap[email]; } async function saveEmailForUser(email: string, userId: string) { - // TODO: Save email and userId mapping + // TODO: Save email and userId mapping - // this is just a placeholder implementation - emailUserMap[email] = userId + // this is just a placeholder implementation + emailUserMap[email] = userId; } function isInputEmail(input: string): boolean { - return input.match( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ) !== null; + return ( + input.match( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ) !== null + ); } async function getEmailUsingUserId(userId: string) { - // TODO: check your database mapping.. - - // this is just a placeholder implementation - let emails = Object.keys(emailUserMap) - for (let i = 0; i < emails.length; i++) { - if (emailUserMap[emails[i]] === userId) { - return emails[i] - } + // TODO: check your database mapping.. + + // this is just a placeholder implementation + let emails = Object.keys(emailUserMap); + for (let i = 0; i < emails.length; i++) { + if (emailUserMap[emails[i]] === userId) { + return emails[i]; } - return undefined; + } + return undefined; } SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - override: { - /* ...from previous code snippets... */ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + /* ...from previous code snippets... */ + }, + signUpFeature: { + formFields: [ + /* ... from previous code snippet ... */ + ], + }, + emailDelivery: { + override: (original) => { + return { + ...original, + sendEmail: async function (input) { + input.user.email = (await getEmailUsingUserId(input.user.id))!; + return original.sendEmail(input); }, - signUpFeature: { - formFields: [ /* ... from previous code snippet ... */] - }, - // highlight-start - emailDelivery: { - override: (original) => { - return { - ...original, - sendEmail: async function (input) { - input.user.email = (await getEmailUsingUserId(input.user.id))!; - return original.sendEmail(input) - } - } - } - }, - // highlight-end - }) - ] + }; + }, + }, + }), + ], }); ``` - - - - + + ```go import ( "regexp" @@ -1572,7 +1517,6 @@ func main() { return originalImplementation }, }, - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail @@ -1587,17 +1531,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from re import fullmatch from typing import Any, Dict @@ -1646,7 +1587,6 @@ async def get_email_using_user_id(user_id: str): return email return None -# highlight-start def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email @@ -1659,13 +1599,12 @@ def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> original_implementation.send_email = send_email return original_implementation -# highlight-end init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( sign_up_feature=InputSignUpFeature(form_fields=[ @@ -1675,23 +1614,19 @@ init( # functions=..., from previous code snippet # apis=..., from previous code snippet ), - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` - - - - + + ### 5. Show the new fields in the user interface -:::info -The following instructions are only relevant if you are using the pre-built UI. +:::info[The following instructions are only relevant if you are using the pre-built UI.] If you created your own custom UI on the frontend, please make sure to pass the new email `formField` when you call the sign up function. Even if the user has not given an email, you must add it with an empty string. ::: @@ -1705,70 +1640,70 @@ Update the pre-built UI to reflect the new flow: - Update translations for the email field if necessary. ```tsx -import SuperTokens from "supertokens-auth-react" -import EmailPassword from "supertokens-auth-react/recipe/emailpassword" +import SuperTokens from "supertokens-auth-react"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + languageTranslations: { + translations: { + en: { + EMAIL_PASSWORD_EMAIL_LABEL: "Username or email", + }, }, - // highlight-start - languageTranslations: { - translations: { - "en": { - EMAIL_PASSWORD_EMAIL_LABEL: "Username or email" - } - } - }, - // highlight-end - recipeList: [ - EmailPassword.init({ - signInAndUpFeature: { - // highlight-start - signInForm: { - formFields: [{ - id: "email", - label: "Username or email", - placeholder: "Username or email" - }] - }, - signUpForm: { - formFields: [{ - id: "email", - label: "Username", - placeholder: "Username", - validate: async (input) => { - // the backend validates this anyway. So nothing required here - return undefined; - } - }, { - id: "actualEmail", - validate: async (input) => { - // the backend validates this anyway. So nothing required here - return undefined - }, - label: "Email", - optional: true - }] - } - // highlight-end - } - }), - // other recipes initialisation.. - ], + }, + recipeList: [ + EmailPassword.init({ + signInAndUpFeature: { + signInForm: { + formFields: [ + { + id: "email", + label: "Username or email", + placeholder: "Username or email", + }, + ], + }, + signUpForm: { + formFields: [ + { + id: "email", + label: "Username", + placeholder: "Username", + validate: async (input) => { + // the backend validates this anyway. So nothing required here + return undefined; + }, + }, + { + id: "actualEmail", + validate: async (input) => { + // the backend validates this anyway. So nothing required here + return undefined; + }, + label: "Email", + optional: true, + }, + ], + }, + }, + }), + // other recipes initialisation.. + ], }); ``` - ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/email-password/initial-setup.mdx b/docs/authentication/email-password/initial-setup.mdx new file mode 100644 index 0000000000..0a1296f2e8 --- /dev/null +++ b/docs/authentication/email-password/initial-setup.mdx @@ -0,0 +1,2784 @@ +--- +title: Set Up Email/Password Authentication +description: Add email and password authentication to your frontend and backend, then prepare the integration for production. +sidebar: + label: Initial Setup + order: 15 +--- + +## Overview + + +Inspect this repository and integrate SuperTokens into the existing application. First discover the frontend stack and backend stack, including languages, frameworks, package managers, routers, SDK versions, existing authentication code, and environment configuration. If the frontend or backend stack cannot be determined reliably, ask the user to provide it before making changes. Also ask which authentication methods and UI approach they need if those choices cannot be inferred. Use the current SuperTokens documentation and SDK APIs, preserve the project's conventions, and do not commit secrets. Configure the frontend, backend, sessions, routes, middleware, cookies, CORS, and environment variables as required. Run the relevant typechecks, tests, and build, then summarize changed files, required environment variables, and validation results. + + +This guide walks through adding Email/Password authentication with either the SuperTokens prebuilt UI or your own custom UI. Configure the frontend first, then connect your backend and prepare the integration for production. + +## Steps + +### 1. Integrate the frontend SDK + + + +#### Frontend integration summary + +- React uses `supertokens-auth-react`; Angular and Vue use `supertokens-web-js`. +- Initialize the authentication and Session recipes. React applications also wrap their component tree with `SuperTokensWrapper`. +- Render the prebuilt login UI on `/auth`. +- The SDK intercepts `fetch` and XHR requests to manage session tokens automatically. Web sessions use HTTP-only cookies by default, with header-based authentication available as an alternative. + + + +Start the setup by configuring your frontend application to use **SuperTokens** for authentication. + +This guide uses the **SuperTokens pre-built UI** components. +If you want to create your own interface please check the **Custom UI** tutorial. + + + + + +#### 1.1 Install the SDK + +Run the following command in your terminal to install the package. + + +```bash title="Reactjs" option="package-managers:npm" + npm i -s supertokens-auth-react +``` + +```bash title="Reactjs" option="package-managers:yarn" +yarn add supertokens-auth-react supertokens-web-js +``` + +```bash title="Reactjs" option="package-managers:pnpm" +pnpm add supertokens-auth-react supertokens-web-js +``` + +```bash title="Reactjs" option="package-managers:bun" +bun add supertokens-auth-react supertokens-web-js +``` + +```bash title="Angular" option="package-managers:npm" +npm i -s supertokens-web-js +``` + +```bash title="Angular" option="package-managers:yarn" +yarn add supertokens-web-js +``` + +```bash title="Angular" option="package-managers:pnpm" +pnpm add supertokens-web-js +``` + +```bash title="Angular" option="package-managers:bun" +bun add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:npm" +npm i -s supertokens-web-js +``` + +```bash title="Vue" option="package-managers:yarn" +yarn add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:pnpm" +pnpm add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:bun" +bun add supertokens-web-js +``` + + +#### 1.2 Initialize the SDK + + + + In your main application file call the `SuperTokens.init` function to initialize the SDK. + The `init` call includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. + After that you have to wrap the application with the `SuperTokensWrapper` component. + This provides authentication context for the rest of the UI tree. + + + Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. + + **Architecture** + + - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. + - You have to create a `/auth*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. + + **Creating the `/auth` route** + + - Use the Angular CLI to generate a new route + + + Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app + + **Architecture** + + - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. + - We create a `/auth*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. + + **Creating the `/auth` route** + + - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: + + + + +```tsx title="Reactjs" +import React from "react"; + +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import Session from "supertokens-auth-react/recipe/session"; + +SuperTokens.init({ + appInfo: { + // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [EmailPassword.init(), Session.init()], +}); + +/* Your App */ +class App extends React.Component { + render() { + return {/*Your app components*/}; + } +} +``` + +```bash title="Angular" + ng generate module auth --route auth --module app.module +``` + +```tsx check=false reason="This is a Vue single-file component containing both TypeScript and template markup." title="Vue" + + + +``` + + + + + + - Add the following code to your `auth` angular component + + + - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. + + - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. + + + + +```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" + import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; + import { DOCUMENT } from "@angular/common"; + + @Component({ + selector: "app-auth", + template: '
', + }) + export class AuthComponent implements OnDestroy, AfterViewInit { + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + ) {} + + ngAfterViewInit() { + this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@v0.48.0/build/static/js/main.81589a39.js"); + } + + ngOnDestroy() { + // Remove the script when the component is destroyed + const script = this.document.getElementById("supertokens-script"); + if (script) { + script.remove(); + } + } + + private loadScript(src: string) { + const script = this.renderer.createElement("script"); + script.type = "text/javascript"; + script.src = src; + script.id = "supertokens-script"; + script.onload = () => { + supertokensUIInit("supertokensui", { + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [supertokensUIEmailPassword.init(), supertokensUISession.init()], + }); + }; + this.renderer.appendChild(this.document.body, script); + } + } +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" + import { createApp } from "vue"; + import SuperTokens from "supertokens-web-js"; + import Session from "supertokens-web-js/recipe/session"; + import App from "./App.vue"; + import router from "./router"; + + SuperTokens.init({ + appInfo: { + appName: "", + apiDomain: "", + apiBasePath: "/auth", + }, + recipeList: [Session.init()], + }); + + const app = createApp(App); + + app.use(router); + + app.mount("#app"); +``` +
+ + + + + - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. + + - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. + + + + + +```tsx title="Angular" +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; + +SuperTokens.init({ + appInfo: { + appName: "", + apiDomain: "", + apiBasePath: "/auth", + }, + recipeList: [Session.init()], +}); +``` + + +#### 1.3 Configure routing + + + + In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. + The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. + Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. + + + + Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. + + + Add the route handling shown below to your root-level `render` function. + + + + + Update your angular router so that all auth related requests load the `auth` component + + + Update your Vue router so that all auth related requests load the `AuthView` component + + + + +```tsx title="Reactjs" option="react-router:yes" +import React from "react"; +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; + +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import * as reactRouterDom from "react-router-dom"; + +class App extends React.Component { + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } +} +``` + +```tsx title="Reactjs" option="react-router:no" +import React from "react"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; + +class App extends React.Component { + render() { + if (canHandleRoute([EmailPasswordPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([EmailPasswordPreBuiltUI]); + } + + return {/*Your app*/}; + } +} +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" + import { NgModule } from "@angular/core"; + import { RouterModule, Routes } from "@angular/router"; + + const routes: Routes = [ + { + path: "auth", + loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), + }, + + { + path: "**", + loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), + }, + ]; + + @NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + }) + export class AppRoutingModule {} +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" + import { createRouter, createWebHistory } from "vue-router"; + import HomeView from "../views/HomeView.vue"; + import AuthView from "../views/AuthView.vue"; + + const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + name: "home", + component: HomeView, + }, + { + path: "/auth/:pathMatch(.*)*", + name: "auth", + component: AuthView, + }, + ], + }); + + export default router; +``` + + + + + + + :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] + Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. + ::: + + + + + + + + + +```tsx title="Reactjs" option="react-router:yes" +import React from "react"; + +import { BrowserRouter, useRoutes } from "react-router-dom"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import * as reactRouterDom from "react-router-dom"; + +function AppRoutes() { + const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ + ]); + + const routes = useRoutes([ + ...authRoutes.map((route) => route.props), + // Include the rest of your app routes + ]); + + return routes; +} + +function App() { + return ( + + + + + + ); +} +``` + + +#### 1.4 Handle session tokens + +This part is handled automatically by the **Frontend SDK**. +You don't need to do anything. +The step serves more as a way for us to tell you how is this handled under the hood. + +After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. +The interceptors save the session tokens that are generated from the authentication flow. +Those tokens are then added to requests initialized by your frontend app which target the backend API. +By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/post-authentication/session-management/switch-between-cookies-and-header-authentication). + +#### 1.5 Secure application routes + +In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. +Follow the code samples below to understand how to do this. + + + + You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. + + + You can use the `doesSessionExist` function to check if a session exists in all your routes. + + + You can use the `doesSessionExist` function to check if a session exists in all your routes. + + + + +```tsx check=false reason="Requires surrounding quickstart application context" title="Reactjs" +import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import MyDashboardComponent from "./dashboard"; + +class App extends React.Component { + render() { + return ( + + + + {/*Components that require to be protected by authentication*/} + + + } + /> + + + ); + } +} +``` + +```tsx title="Angular" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```tsx title="Vue" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + + +
+ + + +#### 1.1 Install the SDK + +Use the following command to install the required package. + + + + + + + + + :::info + If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/authentication/unified-login/introduction). + ::: + + + + Add to your `settings.gradle`: + + ##### Using CocoaPods + + Add the CocoaPods dependency to your `Podfile` + + Add the dependency to your pubspec.yaml + + + + + +```bash title="Web" option="install-method:npm" +npm i -s supertokens-web-js +``` + +```bash title="Mobile" option="mobile-frameworks:reactnative" +npm i -s supertokens-react-native@5.1.5 @react-native-async-storage/async-storage@2.2.0 +``` + +```bash title="Mobile" option="mobile-frameworks:android" +dependencyResolutionManagement { + ... + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` + +```bash title="Mobile" option="mobile-frameworks:ios" +pod 'SuperTokensIOS', '0.4.2' +``` + +```bash title="Mobile" option="mobile-frameworks:flutter" +supertokens_flutter: 0.6.5 +``` + + + + + + + + + Add the following to you app level's `build.gradle`: + + + ##### Using Swift Package Manager + + Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + + When adding the dependency, select version `0.4.2` after you enter the SuperTokens iOS repository URL: + + + You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). + + + + + + +```bash title="Mobile" option="mobile-frameworks:android" +implementation 'com.github.supertokens:supertokens-android:0.5.3' +``` + +```bash title="Mobile" option="mobile-frameworks:ios" +https://github.com/supertokens/supertokens-ios +``` + + + + + + + + + You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). + + + + + + + +#### 1.2 Initialize SuperTokens + + +Call the SDK init function at the start of your application. +The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. + + + + + + + + Add the `SuperTokens.init` function call at the start of your application. + + + + + + + + +```tsx title="Web" option="install-method:npm" +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; + +SuperTokens.init({ + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [Session.init(), EmailPassword.init()], +}); +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +SuperTokens.init({ + apiDomain: "", + apiBasePath: "/auth", +}); +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + override fun onCreate() { + super.onCreate() + + SuperTokens.Builder(this, "") + .apiBasePath("/auth") + .build() + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + try SuperTokens.initialize( + apiDomain: "", + apiBasePath: "/auth" + ) + } catch SuperTokensError.initError(let message) { + // TODO: Handle initialization error + } catch { + // Some other error + } + + return true + } + +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +void main() { + SuperTokens.init( + apiDomain: "", + apiBasePath: "/auth", + ); +} +``` + + + +#### 1.3 Add the login UI + +The **Email/Password** flow involves two types of user interfaces. +One for registering and creating new users, the *Sign Up Form*. +And one for the actual authentication attempt, the *Sign In Form*. +If you are provisioning users from a different method you can skip over adding the sign up form. + +##### 1.3.1 Add the sign-up form + + + + For the **Sign Up** flow you have to first add the UI elements which render your form. + After that, call the following function when the user submits the form that you have previously created. + + + For the **Sign Up** flow you have to first add the UI elements which render your form. + After that, call the following API when the user submits the form that you have previously created. + + + + +```tsx title="Web" option="install-method:npm" +import { signUp } from "supertokens-web-js/recipe/emailpassword"; + +async function signUpClicked(email: string, password: string) { + try { + let response = await signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + // one of the input formFields failed validation + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax), + // or the email is not unique. + window.alert(formField.error); + } else if (formField.id === "password") { + // Password validation failed. + // Maybe it didn't match the password strength + window.alert(formField.error); + } + }); + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason); + } else { + // sign up successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/homepage"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request POST '/auth/signup' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "formFields": [{ + "id": "email", + "value": "john@example.com" + }, { + "id": "password", + "value": "somePassword123" + }] +}' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. + - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: + - The email could fail validation if it's syntactically not an email, of it it's not unique. + - The password could fail validation if it's not string enough (as defined by the backend password validator). + + Either way, you want to show the user an error next to the input form field. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. + + + +The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: + +```json +{ + "formFields": [ + { + "id": "email", + "value": "john@example.com" + }, + { + "id": "password", + "value": "somePassword123" + }, + { + "id": "name", + "value": "John Doe" + } + ] +} +``` + +On the backend, the `formFields` array is available to you for consumption. + +On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. + +###### How to check if an email is unique + +As a part of the sign up form, you may want to explicitly check that the entered email is unique. +Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. + + + +```tsx title="Web" option="install-method:npm" +import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; + +async function checkEmail(email: string) { + try { + let response = await doesEmailExist({ + email, + }); + + if (response.doesExist) { + window.alert("Email already exists. Please sign in instead"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request GET '/auth/emailpassword/email/exists?email=john@example.com' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + + + +##### 1.3.2 Add the sign-in form + + + + + For the **Sign In** flow you have to first add the UI elements which render your form. + After that, call the following function when the user submits the form that you have previously created. + + + For the **Sign In** flow you have to first add the UI elements which render your form. + After that, call the following API when the user submits the form that you have previously created. + + + + +```tsx title="Web" option="install-method:npm" +import { signIn } from "supertokens-web-js/recipe/emailpassword"; + +async function signInClicked(email: string, password: string) { + try { + let response = await signIn({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax). + window.alert(formField.error); + } + }); + } else if (response.status === "WRONG_CREDENTIALS_ERROR") { + window.alert("Email password combination is incorrect."); + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason); + } else { + // sign in successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/homepage"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request POST '/auth/signin' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "formFields": [{ + "id": "email", + "value": "john@example.com" + }, { + "id": "password", + "value": "somePassword123" + }] +}' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. + - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. + - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. + + + +On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. + +#### 1.4 Handle session tokens + +You can use sessions with SuperTokens in two modes: +- Using `httpOnly` cookies +- Authorization bearer token. + +Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. +For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. + +##### With the Frontend SDK + + + + :::success[No action required.] + ::: + + Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. + + Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. + + By default, our web SDKs use cookies to provide credentials. + + + + + Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + ###### Axios + + ###### Using a custom Axios instance + + + ###### HttpURLConnection + + + ###### `URLSession` + + ###### Using `URLSession.shared` + + + ###### `http` + + You can make requests as you normally would with `http`, the only difference is that you import the client from the SuperTokens package instead. + + + + + + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import axios from "axios"; +import SuperTokens from "supertokens-react-native"; + +let axiosInstance = axios.create({ + /*...*/ +}); +SuperTokens.addAxiosInterceptors(axiosInstance); + +async function callAPI() { + // use axios as you normally do + let response = await axiosInstance.get("https://yourapi.com"); +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens +import com.supertokens.session.SuperTokensHttpURLConnection +import com.supertokens.session.SuperTokensPersistentCookieStore +import java.net.URL +import java.net.HttpURLConnection + +class MainApplication: Application() { + override fun onCreate() { + super.onCreate() + // TODO: Make sure to call SuperTokens.init + } + + fun makeRequest() { + val url = URL("") + val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { + override fun doAction(con: HttpURLConnection?) { + // TODO: Use `con` to set request method, headers etc + } + }) + + // Handle response using connection object, for example: + if (connection.responseCode == 200) { + // TODO: implement + } + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + URLProtocol.registerClass(SuperTokensURLProtocol.self) + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:http/http.dart' as base_http; +import 'package:supertokens_flutter/http.dart' as supertokens_http; + +Future makeRequest() async { + Uri uri = Uri.parse("http://localhost:3001/api"); + var response = await http.get(uri); + // handle response +} +``` + + + + + + + + ###### Using the global Axios instance + + :::note[You must call `addAxiosInterceptors` on all `axios` imports.] + ::: + + + :::note[When making network requests you do not need to call `HttpURLConnection.connect` because SuperTokens does this for you.] + ::: + + ###### OkHttp or Retrofit + + + ###### Using a custom `URLSession` instance + + + ###### Using a custom HTTP client + + If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. + + + + + + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import axios from "axios"; +import SuperTokens from "supertokens-react-native"; +SuperTokens.addAxiosInterceptors(axios); + +async function callAPI() { + // use axios as you normally do + let response = await axios.get("https://yourapi.com"); +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.content.Context +import com.supertokens.session.SuperTokens +import com.supertokens.session.SuperTokensInterceptor +import okhttp3.OkHttpClient +import retrofit2.Retrofit + +class NetworkManager { + fun getClient(context: Context): OkHttpClient { + val clientBuilder = OkHttpClient.Builder() + clientBuilder.addInterceptor(SuperTokensInterceptor()) + // TODO: Make sure to call SuperTokens.init + + val client = clientBuilder.build() + + // REQUIRED FOR RETROFIT ONLY + val instance = Retrofit.Builder() + .baseUrl("") + .client(client) + .build() + + return client + } + + fun makeRequest(context: Context) { + val client = getClient(context) + // Use client to make requests normally + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [SuperTokensURLProtocol.self] + let session = URLSession(configuration: configuration) + + // Use session when making network requests + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +// Import http from the SuperTokens package +import 'package:supertokens_flutter/http.dart' as http; + +Future makeRequest() async { + Uri uri = Uri.parse("http://localhost:3001/api"); + + var customClient = base_http.Client(); + var httpClient = supertokens_http.Client(client: customClient); + + var response = await httpClient.get(uri); + // handle response +} +``` + + + + + + + + ###### Fetch + + :::success[When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here.] + ::: + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + ###### Alamofire + + + ###### Dio + + ###### Add the SuperTokens interceptor + + Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. + + + + + + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS +import Alamofire + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + let configuration = URLSessionConfiguration.af.default + configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) + let session = Session(configuration: configuration) + + // Use session when making network requests + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/dio.dart'; +import 'package:dio/dio.dart'; + +void setup() { + Dio dio = Dio(); // Create a Dio instance. + dio.addSupertokensInterceptor(); +} +``` + + + + + + + + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + ###### Making network requests + + You can make requests as you normally would with `dio`. + + + + + + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/dio.dart'; +import 'package:dio/dio.dart'; + +void setup() { + Dio dio = Dio( + // Provide your config here + ); + dio.addSupertokensInterceptor(); + + var response = dio.get("http://localhost:3001/api"); + // handle response +} +``` + + + + + + + + + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + + + +##### Without the Frontend SDK + +:::warning[We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time.] +::: + +In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. + +For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. + + + + + +###### During the Login Action + +You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". + +The login API returns the following headers: +- `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. +- `front-token` header: This contains information about the access token: + - The userID + - The expiry time of the access token + - The payload added by you in the access token. + + Here is the structure of the token: + ```tsx + let frontTokenFromRequestHeader = "..."; + let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); + console.log(frontTokenDecoded); + /* + { + ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required + uid: "....", // user ID + up: { + sub: "..", + iat: .., + ... // other access token payload + } + } + + */ + ``` + + This token is mainly used for cookie-based authentication because you don't have access to the actual access token on the frontend. You may still want to read its payload, for example to adjust the UI based on the user's role. The token is not signed and must not be used for authorization. If you cache it, treat its contents as untrusted and clear it when the session ends. + +- `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. + +###### When You Make Network Requests to Protected APIs + +The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: +- `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. +- `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. +- For cross-origin browser requests, set the Fetch `credentials` request option to `"include"` (or the equivalent option in your HTTP library). `credentials` is not an HTTP header and does not accept `true` in Fetch. + +An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: +- `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. +- `front-token`: This should be read and saved by you in the same way as it's being done during login. + +###### Handling session refreshing + +If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. + +You can call the refresh API as follows: + + +```bash +curl --location --request POST '/auth/session/refresh' \ +--header 'Cookie: sRefreshToken=...' +``` + +:::note[You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in.] +- The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. +::: + + +The result of a session refresh is either: +- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. +- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. + + + + + +###### During the Login Action + +You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". + +The login API returns the following headers: +- `st-access-token`: This contains the current access token associated with the session. +- `st-refresh-token`: This contains the current refresh token associated with the session. + +Do not persist these tokens in browser `localStorage`, because injected scripts can read them. Prefer the Web SDK's cookie-based mode for browsers. Native applications should use platform-provided secure storage. If you manually use header-based authentication in a browser, keep tokens in memory and account for the session ending when the page reloads. + +###### When You Make Network Requests to Protected APIs + +You need to add the following headers to request: +- `authorization: Bearer {access-token}` +- Header-based requests do not require the Fetch API's `credentials` option unless the request also relies on cookies or HTTP authentication. + +An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` + +These should be read and saved by you in the same way as it's being done during login. + +###### Handling session refreshing + +If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. + +You can call the refresh API as follows: + + +```bash +curl --location --request POST '/auth/session/refresh' \ +--header 'authorization: Bearer {refresh-token}' +``` + + +The result of a session refresh is either: +- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. +- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. + + + + + + +#### 1.5 Protect frontend routes + +You can use the `doesSessionExist` function to check if a session exists. + + + +```tsx title="Web" option="install-method:npm" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +async function doesSessionExist() { + if (await SuperTokens.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + fun doesSessionExist() { + if (SuperTokens.doesSessionExist(this.applicationContext)) { + // user is logged in + } else { + // user has not logged in yet + } + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func doesSessionExist() { + if SuperTokens.doesSessionExist() { + // User is logged in + } else { + // User is not logged in + } + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +Future doesSessionExist() async { + return await SuperTokens.doesSessionExist(); +} +``` + + + + +#### 1.6 Add a sign-out action + +The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. + + + +```tsx title="Web" option="install-method:npm" +import Session from "supertokens-web-js/recipe/session"; + +async function logout() { + await Session.signOut(); + window.location.href = "/auth"; // or to wherever your logic page is +} +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +async function logout() { + await SuperTokens.signOut(); + // navigate to the login screen.. +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + fun logout() { + SuperTokens.signOut(this); + // navigate to the login screen.. + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func signOut() { + SuperTokens.signOut(completionHandler: { + error in + + if error != nil { + // handle error + } else { + // Signed out successfully + } + }) + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +Future signOut() async { + await SuperTokens.signOut( + completionHandler: (error) { + // handle error if any + } + ); +} +``` + + + +- On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. +- The `signOut` function calls the sign out API exposed by the session recipe on the backend. +- If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session. + + + + +### 2. Integrate the backend SDK + +Let's go through the changes required so that your backend can expose the **SuperTokens** authentication features. + +#### 2.1 Install the backend SDK + +Run the following command in your terminal to install the package. + + +```bash title="Node.js" option="package-managers:npm" +npm i -s supertokens-node +``` + +```bash title="Node.js" option="package-managers:yarn" +yarn add supertokens-node +``` + +```bash title="Node.js" option="package-managers:pnpm" +pnpm add supertokens-node +``` + +```bash title="Node.js" option="package-managers:bun" +bun add supertokens-node +``` + +```bash title="Go" +go get github.com/supertokens/supertokens-golang +``` + +```bash title="Python" +pip install supertokens-python +``` + + +:::info[Official backend SDKs are available for **Node.js**, **Python**, and **Go**.] +For other languages, create a separate authentication service. Our [other frameworks guide](/references/backend-sdks/other-frameworks) explains this approach. + +::: + +#### 2.2 Initialize the backend SDK + + +You will have to initialize the **Backend SDK** alongside the code that starts your server. +The init call will include [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. + + +```tsx title="Node.js" option="node-frameworks:express" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "hapi", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "fastify", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "koa", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "loopback", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```go title="Go" + import ( + "github.com/supertokens/supertokens-golang/recipe/emailpassword" + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/supertokens" + ) + + func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + emailpassword.Init(nil), + session.Init(nil), + }, + }) + + if err != nil { + panic(err.Error()) + } + } +``` + +```python title="Python" option="python-frameworks:fastapi" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + +```python title="Python" option="python-frameworks:flask" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='flask', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ] +) +``` + +```python title="Python" option="python-frameworks:django" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='django', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ], + mode='asgi' # use wsgi if you are running django server in sync mode +) +``` + + + +:::info[Multiple frontend domains] +To handle clients from different domains with the same SuperTokens instance, use the `origin` property in the `appInfo` object instead of `websiteDomain`. +The property accepts a function that receives the original request as an input and should return a valid domain. +Make sure to whitelist all the domains during CORS configuration. + +Keep in mind that with this setup, each frontend application will not share authentication sessions. +Users will have to authenticate separately for each domain. +To configure a shared authentication experience between multiple services check the [Unified Login](/authentication/unified-login/introduction) documentation. +::: + + +#### 2.3 Add the SuperTokens APIs and configure CORS + +Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. +Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. + + + + + Register the `plugin`. + + Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. + + + :::note[Add the `middleware` BEFORE all your routes.] + ::: + + + :::note[Add the `middleware` BEFORE all your routes.] + ::: + + + + + Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. + + + + + Use the `Middleware` (**BEFORE all your routes**) and the `get_all_cors_headers()` functions as shown below. + + + - Use the `Middleware` (**BEFORE all your routes and after calling init function**) and the `get_all_cors_headers()` functions as shown below. + - Add a route to catch all paths and return a 404. This is needed because if we don't add this, then OPTIONS request for the APIs exposed by the `Middleware` will return a `404`. + + + Use the `Middleware` and the `get_all_cors_headers()` functions as shown below in your `settings.py`. + + + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/express"; + +let app = express(); + +app.use( + cors({ + origin: "", + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }), +); + +// IMPORTANT: CORS should be before the below line. +app.use(middleware()); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import Hapi from "@hapi/hapi"; +import supertokens from "supertokens-node"; +import { plugin } from "supertokens-node/framework/hapi"; + +let server = Hapi.server({ + port: 8000, + routes: { + cors: { + origin: [""], + additionalHeaders: [...supertokens.getAllCORSHeaders()], + credentials: true, + }, + }, +}); + +(async () => { + await server.register(plugin); + + await server.start(); +})(); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import cors from "@fastify/cors"; +import supertokens from "supertokens-node"; +import { plugin } from "supertokens-node/framework/fastify"; +import formDataPlugin from "@fastify/formbody"; + +import fastifyImport from "fastify"; + +let fastify = fastifyImport(); + +// ...other middlewares +fastify.register(cors, { + origin: "", + allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], + credentials: true, +}); + +(async () => { + await fastify.register(formDataPlugin); + await fastify.register(plugin); + + await fastify.listen({ port: 8000 }); +})(); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import Koa from "koa"; +import cors from "@koa/cors"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/koa"; + +let app = new Koa(); + +app.use( + cors({ + origin: "", + allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }), +); + +app.use(middleware()); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import { RestApplication } from "@loopback/rest"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/loopback"; + +let app = new RestApplication({ + rest: { + cors: { + origin: "", + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }, + }, +}); + +app.middleware(middleware); + +// ...your API routes +``` + +```go title="Go" option="go-frameworks:http" +import ( + "net/http" + "strings" + + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + http.ListenAndServe("SERVER ADDRESS", corsMiddleware( + supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, + r *http.Request) { + // TODO: Handle your APIs.. + + })))) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { + response.Header().Set("Access-Control-Allow-Origin", "") + response.Header().Set("Access-Control-Allow-Credentials", "true") + if r.Method == "OPTIONS" { + // we add content-type + other headers used by SuperTokens + response.Header().Set("Access-Control-Allow-Headers", + strings.Join(append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...), ",")) + response.Header().Set("Access-Control-Allow-Methods", "*") + response.Write([]byte("")) + } else { + next.ServeHTTP(response, r) + } + }) +} +``` + +```go title="Go" option="go-frameworks:gin" +import ( + "net/http" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + router := gin.New() + + // CORS + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{""}, + AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}, + AllowHeaders: append([]string{"content-type"}, + supertokens.GetAllCORSHeaders()...), + AllowCredentials: true, + })) + + // Adding the SuperTokens middleware + router.Use(func(c *gin.Context) { + supertokens.Middleware(http.HandlerFunc( + func(rw http.ResponseWriter, r *http.Request) { + c.Next() + })).ServeHTTP(c.Writer, c.Request) + // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly + c.Abort() + }) + + // Add APIs and start server +} +``` + +```go title="Go" option="go-frameworks:chi" +import ( + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + r := chi.NewRouter() + + // CORS + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{""}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...), + AllowCredentials: true, + })) + + // SuperTokens Middleware + r.Use(supertokens.Middleware) + + // Add APIs and start server +} +``` + +```go title="Go" option="go-frameworks:mux" +import ( + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + // TODO: Add APIs + + router := mux.NewRouter() + + // Adding handlers.CORS(options)(supertokens.Middleware(router))) + http.ListenAndServe("SERVER ADDRESS", handlers.CORS( + handlers.AllowedHeaders(append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...)), + handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), + handlers.AllowedOrigins([]string{""}), + handlers.AllowCredentials(), + )(supertokens.Middleware(router))) +} +``` + +```python title="Python" option="python-frameworks:fastapi" +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware + +from supertokens_python import get_all_cors_headers +from supertokens_python.framework.fastapi import get_middleware + +app = FastAPI() +app.add_middleware(get_middleware()) + +# TODO: Add APIs + +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "" + ], + allow_credentials=True, + allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +# TODO: start server +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" +from supertokens_python import get_all_cors_headers +from flask import Flask, abort +from flask_cors import CORS +from supertokens_python.framework.flask import Middleware + +app = Flask(__name__) +Middleware(app) + +# TODO: Add APIs + +CORS( + app=app, + origins=[ + "" + ], + supports_credentials=True, + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +# This is required since if this is not there, then OPTIONS requests for +# the APIs exposed by the supertokens' Middleware will return a 404 +@app.route('/', defaults={'u_path': ''}) +@app.route('/') +def catch_all(u_path: str): + abort(404) + +# TODO: start server +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" +from typing import List + +from corsheaders.defaults import default_headers + +from supertokens_python import get_all_cors_headers + +CORS_ORIGIN_WHITELIST = [ + "" +] + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOWED_ORIGINS = [ + "" +] + +CORS_ALLOW_HEADERS: List[str] = list(default_headers) + [ + "Content-Type" +] + get_all_cors_headers() + +INSTALLED_APPS = [ + 'corsheaders', + 'supertokens_python' +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + ..., + 'supertokens_python.framework.django.django_middleware.middleware', +] +# TODO: start server +``` + + +You can review all the endpoints that are added through the use of **SuperTokens** by visiting the [API Specs](https://app.swaggerhub.com/apis/supertokens/FDI). + +#### 2.4 Add the SuperTokens error handler + +Depending on the language and framework that you are using, you might need to add a custom error handler to your server. +The handler will catch all the authentication related errors and return proper HTTP responses that can be parsed by the frontend SDKs. + + + + + + No additional `errorHandler` is required. + + Add the `errorHandler` **Before all your routes and plugin registration** + + No additional `errorHandler` is required. + No additional `errorHandler` is required. + + + + :::info[You can skip this step] + ::: + + + :::info[You can skip this step] + ::: + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express, { Request, Response, NextFunction } from "express"; +import { errorHandler } from "supertokens-node/framework/express"; + +let app = express(); + +// ...your API routes + +// Add this AFTER all your routes +app.use(errorHandler()); + +// your own error handler +app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { + /* ... */ +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import Fastify from "fastify"; +import { errorHandler } from "supertokens-node/framework/fastify"; + +let fastify = Fastify(); + +fastify.setErrorHandler(errorHandler()); + +// ...your API routes +``` + + +#### 2.5 Secure application routes + +Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. + + + + For your APIs that require a user to be logged in, use the `verifySession` middleware. + + + For your APIs that require a user to be logged in, use the `VerifySession` middleware. + + + For your APIs that require a user to be logged in, use the `verify_session` middleware. + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express from "express"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; + +let app = express(); + +app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //.... +}); +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import Hapi from "@hapi/hapi"; +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //... + }, +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import Fastify from "fastify"; +import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { SessionRequest } from "supertokens-node/framework/fastify"; + +let fastify = Fastify(); + +fastify.post( + "/like-comment", + { + preHandler: verifySession(), + }, + (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //.... + }, +); +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import KoaRouter from "koa-router"; +import { verifySession } from "supertokens-node/recipe/session/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; + +let router = new KoaRouter(); + +router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { + let userId = ctx.session!.getUserId(); + //.... +}); +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import { inject, intercept } from "@loopback/core"; +import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; +import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; +import { SessionContext } from "supertokens-node/framework/loopback"; + +class LikeComment { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession()) + @response(200) + handler() { + let userId = (this.ctx as SessionContext).session!.getUserId(); + //.... + } +} +``` + +```go title="Go" option="go-frameworks:http" +import ( + "fmt" + "net/http" + + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // Wrap the API handler in session.VerifySession + session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) + }) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:gin" +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" +) + +func main() { + router := gin.New() + + // Wrap the API handler in session.VerifySession + router.POST("/likecomment", verifySession(nil), likeCommentAPI) +} + +// This is a function that wraps the supertokens verification function +// to work the gin +func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc { + return func(c *gin.Context) { + session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) { + c.Request = c.Request.WithContext(r.Context()) + c.Next() + })(c.Writer, c.Request) + // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly + c.Abort() + } +} + +func likeCommentAPI(c *gin.Context) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:chi" +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi" + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + r := chi.NewRouter() + + // Wrap the API handler in session.VerifySession + r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:mux" +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + router := mux.NewRouter() + + // Wrap the API handler in session.VerifySession + router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:fastapi" +from fastapi import Depends + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.fastapi import verify_session + + +@app.post('/like_comment') +async def like_comment(session: SessionContainer = Depends(verify_session())): + user_id = session.get_user_id() + + print(user_id) +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" +from flask import g + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.flask import verify_session + + +@app.route('/update-jwt', methods=['POST']) +@verify_session() +def like_comment(): + session: SessionContainer = g.supertokens + + user_id = session.get_user_id() + + print(user_id) +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" +from typing import cast + +from django.http import HttpRequest + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.django.asyncio import verify_session + + +@verify_session() +async def like_comment(request: HttpRequest): + session: SessionContainer = cast(SessionContainer, request.supertokens) + + user_id = session.get_user_id() + + print(user_id) +``` + + +The middleware function returns a `401` to the frontend if a session doesn't exist, or if the access token has expired, in which case, our frontend SDK automatically refreshes the session. + +In case of successful session verification, you get access to a `session` object using which you can get the user's ID, or manipulate the session information. + +### 3. Configure the Core Service + +If you have signed up and deployed a SuperTokens environment already, you can skip this step. +Otherwise, please follow these instructions to use the correct **SuperTokens Core** instance in your application. + +The steps show you how to connect to a **SuperTokens Managed Service Environment**. +If you want to self host the core instance please check the [following guide](/deployment/self-host-supertokens). + + +#### 3.1 Sign up for a SuperTokens account + +Open this [page](https://supertokens.com/auth) in order to access the account creation page. +Select the account that you want to use and wait for the action to complete. + +#### 3.2 Create a deployment + +After signing in, open the SuperTokens dashboard and select **Managed**. Enter a name for the deployment, select the region closest to your backend services, and click **Deploy Core**. + +Our internal service will deploy a separate environment based on your selection. +After this process is complete, open the new deployment from the list. + +:::info[The initial setup flow only configures a development environment.] +In order to use SuperTokens in production, you will have to create a separate deployment. + +::: + +#### 3.3 Connect the backend SDK with SuperTokens + +In the SuperTokens dashboard, open the newly created deployment and select **Overview**. In **Connection Information**, copy the **Connection URI** and one of the **API Keys**, then use them as `connectionURI` and `apiKey` in your backend SDK configuration. If no suitable key exists, click **Generate Key** to create one. + + + +```tsx title="Node.js" +import supertokens from "supertokens-node"; + +supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], +}); +``` + +```go title="Go" +import "github.com/supertokens/supertokens-golang/supertokens" + +func main() { + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "", + APIKey: "", + }, + }) +} + +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" +from supertokens_python import init, InputAppInfo, SupertokensConfig + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + supertokens_config=SupertokensConfig( + connection_uri='', + api_key='' + ), + framework='...', + recipe_list=[ + #... + ] +) +``` + + +## Next steps + + +Review this repository's SuperTokens integration for production readiness. Inspect Core deployment configuration, API keys, environment separation, HTTPS, secret handling, session security, CORS, cookies, email or SMS delivery, rate limits, logging, and error handling. Check that frontend and backend recipes match and that protected routes are actually protected. Run the relevant tests, typechecks, and build. Report findings by severity with file references, then make only safe fixes that are clearly required. + + +Now that you have completed the initial setup, continue configuring SuperTokens for your application's authentication and authorization requirements. + + + + Add passwordless, social, enterprise, or machine-to-machine authentication. + + + Verify user email addresses during sign-up. + + + Add more authentication factors to your sign-in process. + + + Configure session security, storage, and advanced workflows. + + + Manage users through the SuperTokens Dashboard. + + + Run SuperTokens as a managed service or inside your infrastructure. + + diff --git a/docs/authentication/email-password/introduction.mdx b/docs/authentication/email-password/introduction.mdx index a0f8f3dd94..01ce9bf0f6 100644 --- a/docs/authentication/email-password/introduction.mdx +++ b/docs/authentication/email-password/introduction.mdx @@ -1,97 +1,71 @@ --- -title: Introduction -hide_title: true -sidebar_position: 1 -pagination_next: null -pagination_prev: null -skip_llms_txt: true -description: >- - Authenticate users with email/password, customize UI, and explore SDK - integration options. -page_type: overview -recipe: emailpassword -category: email-password +title: Email/Password Authentication +description: Integrate email/password sign-up, sign-in, sessions, password reset, and optional email verification with prebuilt or custom UI. +sidebar: + label: Overview + order: 10 --- + -# Introduction +## Email/password integration summary + +- Configure the EmailPassword and Session recipes on both the frontend and backend. +- Choose the prebuilt UI or a custom UI and configure the authentication routes. +- Configure password reset and decide whether your application requires email verification. +- Validate sign-up, sign-in, sign-out, session refresh, and password reset before deployment. + + + + +Add SuperTokens email/password authentication to this existing application. First inspect the project stack and current SuperTokens configuration. Configure the frontend and backend EmailPassword and Session recipes, the chosen pre-built or custom UI, auth routes, password reset flow, and environment variables. Ask whether email verification is required if it is not clear. Preserve existing conventions, do not commit secrets, and validate the sign-up, sign-in, sign-out, session refresh, and password reset flows with the relevant tests and build. + ## Overview The **Email/Password** `recipe` provides a way of authenticating users with basic credentials. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. -Sign in form UI for email password login + + Sign in form UI for email password login + -## Getting started +## Getting started You can either follow the quickstart tutorial or use the `CLI` tool to generate an example app that shows you how the recipe works. - - - - Quickstart - - - Go through a quick tutorial that shows you how to add the **Email/Password** recipe to your app. - - - - - Example Applications - - - Use the CLI to generate a boilerplate app that you can use as a starting point. - - - - - -## Customization + + +Go through a quick tutorial that shows you how to add the **Email/Password** recipe to your app. + + -To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Sign In Form Customization - - - Adapt the look and feel of the pre-built sign in form. - - - - - Sign Up Form Customization - - - Adapt the look and feel of the pre-built sign up form. - - - - - Hooks and Overrides - - - Add custom logic after the user logs in or signs up. - - - - - Password Hashing - - - Read which hashing algorithms you can use and how to use them. - - - - - Username Login - - - Discover how you can implement an authentication flow that makes use of usernames instead of email addresses. - - - +## Customization +To adjust the functionality to fit your use case you can explore different sections from the documentation. + + +Adapt the look and feel of the pre-built sign in form. + + +Adapt the look and feel of the pre-built sign up form. + + +Add custom logic after the user logs in or signs up. + + +Read which hashing algorithms you can use and how to use them. + + +Discover how you can implement an authentication flow that makes use of usernames instead of email addresses. + + diff --git a/docs/authentication/email-password/meta.ts b/docs/authentication/email-password/meta.ts new file mode 100644 index 0000000000..76d784b8dd --- /dev/null +++ b/docs/authentication/email-password/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email Password", + icon: "rectangle-ellipsis", + order: 20, +}); diff --git a/docs/authentication/email-password/password-hashing.mdx b/docs/authentication/email-password/password-hashing.mdx index c540b47fec..eab17b8500 100644 --- a/docs/authentication/email-password/password-hashing.mdx +++ b/docs/authentication/email-password/password-hashing.mdx @@ -1,23 +1,14 @@ --- title: Password hashing -hide_title: true -sidebar_position: 5 -description: >- - Learn about the password hashing algorithms used by SuperTokens and how to - customize and calibrate them. -page_type: guide -recipe: emailpassword -category: email-password +description: Learn about the password hashing algorithms used by SuperTokens and how to customize and calibrate them. +sidebar: + order: 50 --- - - -# Password hashing - ## Overview **SuperTokens** supports two password hashing algorithms: `BCrypt` and `Argon2`. -Per current best practices, `Argon2` is the recommended algorithm. +Per current best practices, `Argon2` is the recommended algorithm. However, **SuperTokens** uses `BCrypt` by default since `Argon2` requires custom settings that are specific to the hardware in which the core is running on. ### Hashing time @@ -34,29 +25,28 @@ For example, if you hash a password with `BCrypt`, it verifies using `BCrypt` ev Instructions: - - - - 1. Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. + + +1. Go to the [SuperTokens SaaS dashboard](https://supertokens.com/dashboard) and select the relevant **Managed** deployment. 2. Open **Configuration** and find the **Password Hashing Algorithm** setting. 3. Change the algorithm. Configuration changes are saved automatically. - + + - + + + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e PASSWORD_HASHING_ALG=BCRYPT \ -e BCRYPT_LOG_ROUNDS=11 \ - // highlight-end -d supertokens/supertokens- ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command @@ -64,24 +54,19 @@ docker run \ password_hashing_alg: BCRYPT bcrypt_log_rounds: 11 ``` - - - + + ## Hashing calibration This information is relevant only for self hosted core instances. The managed service instances have already calibrated the algorithms based on the hardware. -:::important -When you change the hashing settings make sure to run the calibration CLI command to find the right balance for your hardware. +:::note[When you change the hashing settings make sure to run the calibration CLI command to find the right balance for your hardware.] ::: - - - - - + + #### Algorithm settings | Name | Default | Description | @@ -92,35 +77,42 @@ When you change the hashing settings make sure to run the calibration CLI comman | `argon2_parallelism` | `2` | This is the number of threads the algorithm uses during hashing. The higher this is, the harder it would be to crack passwords offline using multiple cores. Should be equal to the number of virtual cores (or twice the number of physical cores) available in the system. | | `argon2_hashing_pool_size` | `1` | This is the maximum number of concurrent hashes that the core performs. A value of `1` means that the core does only one hash at one point in time, other requests for hashing queue up and wait for their turn. | - ##### Example If each hash takes 300 milliseconds, a value of `1` here would entail ~ a max of 3 hashes per second (1000 ms / 300 ms). A value of `2` here would entail a max of 6 hashes per second (1000 ms / 300 ms)*2. Password hashing occurs during sign in, sign up, and password reset flows. Therefore, you can set this value according to the target time per hash and how many sign ups/in you expect per second. - #### Change the settings + + +#### Algorithm settings + +| Name | Default | Description | +|------|--------------|-------------| +| `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using bcrypt, set this to `BCRYPT`. | +| `bcrypt_log_rounds` | `11` | The number of rounds to use for hashing is `2^bcrypt_log_rounds`. The higher this value, the more time hashing takes. | - - +#### Change settings + + + + + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e PASSWORD_HASHING_ALG=ARGON2 \ -e ARGON2_ITERATIONS=1 \ -e ARGON2_MEMORY_KB=87795 \ -e ARGON2_PARALLELISM=2 \ -e ARGON2_HASHING_POOL_SIZE=1 \ - // highlight-end -d supertokens/supertokens- ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command @@ -131,14 +123,39 @@ argon2_memory_kb: 87795 argon2_parallelism: 2 argon2_hashing_pool_size: 1 ``` + + + + + + +```bash +docker run \ + -p 3567:3567 \ + -e PASSWORD_HASHING_ALG=BCRYPT \ + -e BCRYPT_LOG_ROUNDS=11 \ + -d supertokens/supertokens- +``` + + +```yaml +# You need to add the following to the config.yaml file. +# The file path can be found by running the "supertokens --help" command - - +password_hashing_alg: BCRYPT +bcrypt_log_rounds: 11 +``` + + + + + + #### Calibrate to your hardware - To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: + - `--with_alg`: - The value of this should be `argon2` - Compulsory parameter @@ -156,25 +173,56 @@ To find the optimal setting for your hardware, you can run the `hashingCalibrate - The default value is `2*number of cores` Running the algorithm takes minutes. + + +#### Calibrate to your hardware - - +To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: +- `--with_alg`: + - The value of this should be `bcrypt`. + - Compulsory parameter +- `--with_time_per_hash_ms`: + - This requires the target time per hash (in milliseconds). + - The default value is `300` + + + + + + + ```bash docker run supertokens/supertokens- supertokens hashingCalibrate --with_alg=argon2 ``` - - - - + + ```bash supertokens hashingCalibrate --with_alg=argon2 ``` - - - + + + + + + +```bash +docker run supertokens/supertokens- supertokens hashingCalibrate --with_alg=bcrypt +``` + + +```bash +supertokens hashingCalibrate --with_alg=bcrypt +``` + + + + The above produces an output like: + + + ```text ====Input Settings==== @@ -211,11 +259,9 @@ Calculating average hashing time.... ..................................................Took 494 MS per hash - <....Truncated....> - Adjusting memory to reach target time. Current argon2 settings @@ -245,90 +291,8 @@ argon2_hashing_pool_size: 1 ==================== You should use these as docker environment variables or put them in the config.yaml file in the SuperTokens installation directory. ``` - -The contents of the `====Final values====` gives you the values of the parameters to provide to the core. - -The algorithm starts with the highest amount of memory per hash (= `with_argon2_max_memory_mb/with_argon2_hashing_pool_size`) and `1` iteration. -It calculates the current average hashing time by simulating hashes concurrently (based on the value of `with_argon2_hashing_pool_size`). If the hashing time is greater than the target time, it reduces the memory by 5%. If it's less than the target time, it increases the number of iterations. The algorithm stops if the current time is within 10 milliseconds (higher or lower) of the target time. - -:::info debug -If you see an output like: -```bash -/usr/bin/supertokens: line 9: 15 Killed "${ST_INSTALL_LOC}"jre/bin/java -classpath "${ST_INSTALL_LOC}cli/*" io.supertokens.cli.Main false "${ST_INSTALL_LOC}" $@ -``` -it means that the system doesn't have enough memory. Try to run the algorithm again with a lower memory value by passing `--with_argon2_max_memory_mb` -::: - - - - - - -#### Algorithm settings - -| Name | Default | Description | -|------|--------------|-------------| -| `password_hashing_alg` | - | This setting chooses which password hashing algorithm to use. For using bcrypt, set this to `BCRYPT`. | -| `bcrypt_log_rounds` | `11` | The number of rounds to use for hashing is `2^bcrypt_log_rounds`. The higher this value, the more time hashing takes. | - -#### Change settings - - - - -```bash -docker run \ - -p 3567:3567 \ - // highlight-start - -e PASSWORD_HASHING_ALG=BCRYPT \ - -e BCRYPT_LOG_ROUNDS=11 \ - // highlight-end - -d supertokens/supertokens- -``` - - - - -```yaml -# You need to add the following to the config.yaml file. -# The file path can be found by running the "supertokens --help" command - -password_hashing_alg: BCRYPT -bcrypt_log_rounds: 11 -``` - - - - - -#### Calibrate to your hardware - -To find the optimal setting for your hardware, you can run the `hashingCalibrate` command via the CLI. This command takes a few parameters: -- `--with_alg`: - - The value of this should be `bcrypt`. - - Compulsory parameter -- `--with_time_per_hash_ms`: - - This requires the target time per hash (in milliseconds). - - The default value is `300` - - - - -```bash -docker run supertokens/supertokens- supertokens hashingCalibrate --with_alg=bcrypt -``` - - - - -```bash -supertokens hashingCalibrate --with_alg=bcrypt -``` - - - - -The above produces an output like: + + ```text ====Input Settings==== -> Target time per hash (--with_time_per_hash_ms): 300 MS @@ -352,21 +316,47 @@ bcrypt_log_rounds: 12 ==================== You should use this as a docker environment variable or put this in the config.yaml file in the SuperTokens installation directory. ``` + + + + + +The contents of the `====Final values====` gives you the values of the parameters to provide to the core. + +The algorithm starts with the highest amount of memory per hash (= `with_argon2_max_memory_mb/with_argon2_hashing_pool_size`) and `1` iteration. +It calculates the current average hashing time by simulating hashes concurrently (based on the value of `with_argon2_hashing_pool_size`). If the hashing time is greater than the target time, it reduces the memory by 5%. If it's less than the target time, it increases the number of iterations. The algorithm stops if the current time is within 10 milliseconds (higher or lower) of the target time. +:::info[debug] +If you see an output like: + + The contents of the `====Final values====` gives you the values of the parameters to provide to the core. The algorithm starts with the minimum recommended value (`11`), and increments it until the average time per hash is greater than the target time. The final value is then equal to the value that yields the closest time per hash as the target one. + + - + + +```bash +/usr/bin/supertokens: line 9: 15 Killed "${ST_INSTALL_LOC}"jre/bin/java -classpath "${ST_INSTALL_LOC}cli/*" io.supertokens.cli.Main false "${ST_INSTALL_LOC}" $@ +``` + + - + + +it means that the system doesn't have enough memory. Try to run the algorithm again with a lower memory value by passing `--with_argon2_max_memory_mb` +::: + + ## See also - - - - - - + + + + + + diff --git a/docs/authentication/email-password/password-managers.mdx b/docs/authentication/email-password/password-managers.mdx index d9a06b485f..f26b9da5cd 100644 --- a/docs/authentication/email-password/password-managers.mdx +++ b/docs/authentication/email-password/password-managers.mdx @@ -1,82 +1,67 @@ --- -id: password-managers title: Password managers -hide_title: true -sidebar_position: 9 -description: >- - Ensure password managers detect authentication fields by disabling shadow DOM - in your application. -page_type: guide -recipe: emailpassword -category: email-password +description: Ensure password managers detect authentication fields by disabling shadow DOM in your application. +sidebar: + order: 90 --- - -# Password managers - - -## Overview +## Overview Styling encapsulation relies on the ["shadow DOM" browser feature](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). Password managers such as Dashlane, LastPass, or OnePassword do not detect authentication forms fields inside shadow DOMs. -Therefore, if you would like to make sure that your end users can use their password managers, you have to disable shadow DOM. +Therefore, if you would like to make sure that your end users can use their password managers, you have to disable shadow DOM. -:::info no-title -These instructions are only relevant if you are using the pre-built UI components. +:::info[These instructions are only relevant if you are using the pre-built UI components.] ::: - - - -```tsx preview="/img/emailpassword/password-manager.png" previewAlt="Demo of a password manager working with prebuilt UI when shadow DOM is not active" + + +```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - useShadowDom: false, - recipeList: [ /* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + useShadowDom: false, + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx preview="/img/emailpassword/password-manager.png" previewAlt="Demo of a password manager working with prebuilt UI when shadow DOM is not active" + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - useShadowDom: false, - recipeList: [ /* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + useShadowDom: false, + recipeList: [ + /* ... */ + ], }); ``` - - - + + - -:::caution -- SuperTokens uses a special attribute to define its styling. Disabling shadow DOM should not impact the rest of your application's styles. Verify that your CSS does not impact how SuperTokens UI appears when disabling Shadow DOM. +:::warning[- SuperTokens uses a special attribute to define its styling. Disabling shadow DOM should not impact the rest of your application's styles. Verify that your CSS does not impact how SuperTokens UI appears when disabling Shadow DOM.] - Shadow DOM is always disabled with Internet Explorer since it does not support it. Similarly, if you intend to support Internet Explorer for your application make sure to verify how SuperTokens UI appears. ::: - ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/email-password/password-reset.mdx b/docs/authentication/email-password/password-reset.mdx index 90cd2219a4..e95cf7c116 100644 --- a/docs/authentication/email-password/password-reset.mdx +++ b/docs/authentication/email-password/password-reset.mdx @@ -1,17 +1,10 @@ --- title: Password reset -hide_title: true -sidebar_position: 7 description: Learn how the password reset functionality works -page_type: guide -recipe: emailpassword -category: email-password +sidebar: + order: 70 --- - -# Password reset - - ## Overview The password reset feature consists of two actions: one in which a user requests a reset password link over email and another where the user sets the new password. @@ -19,38 +12,38 @@ The password reset feature consists of two actions: one in which a user requests ### The password reset forms - + - + The following images show how the password reset forms render when you are using the pre-built UI. - + + + - +You see this if you navigate to `/auth/reset-password`. -You see this if you navigate to `^{appInfo.websiteBasePath}/reset-password`. - - -UI to send password reset email - +
+UI to send password reset email +
-
+
- + You see this if you navigate to `/auth/reset-password?token=TOKEN`. - - -UI to change password - - + +
+UI to change password +
+
-
+ - + To implement your own interface create two different forms: - One where the user requests a password reset link. @@ -59,16 +52,16 @@ To implement your own interface create two different forms: Use the pre-built UI components as a reference. - + -### The password reset email +### The password reset email This is how the email that gets delivered to the learner looks like: -Email UI for password reset email +Email UI for password reset email You can find the [source code of this template on GitHub](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). -To customize the template check the [email delivery](/docs/platform-configuration/email-delivery) section for more information. +To customize the template check the [email delivery](/platform-configuration/email-delivery) section for more information. --- @@ -76,165 +69,150 @@ To customize the template check the [email delivery](/docs/platform-configuratio To embed the reset form in a page you can use the next steps. - - -### 1. Disable the default implementation + - - +### 1. Disable the default implementation + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - disableDefaultUI: true - }, - // highlight-end - }), - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + disableDefaultUI: true, + }, + }), + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - disableDefaultUI: true - }, - // highlight-end - }), - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + resetPasswordUsingTokenFeature: { + disableDefaultUI: true, + }, + }), + ], }); ``` - - - + + If you navigate to `/auth/reset-password`, you should not see the widget anymore. -### 2. Render the component yourself +### 2. Render the component yourself Add the `ResetPasswordUsingToken` component in your app: - - + + +:::warning[You have to build your own UI for this.] +::: + + + + ```tsx import React from "react"; -import {ResetPasswordUsingToken} from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; +import { ResetPasswordUsingToken } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; class ResetPasswordPage extends React.Component { - render() { - return ( -
- // highlight-next-line - -
- ) - } + render() { + return ( +
+ +
+ ); + } } ``` -
- - - -:::caution -You have to build your own UI for this. -::: + + - -
+ + -
- + + -:::caution -Not applicable since you do not use pre-built UI. +:::warning[Not applicable since you do not use pre-built UI.] ::: - + -### 3. Change the website path for reset password UI +### 3. Change the website path for reset password UI This step is optional. -The default path for this is component is `^{appInfo.websiteBasePath}/reset-password`. +The default path for this is component is `/auth/reset-password`. If you are displaying this at some custom path, then you need to add additional configuration on the backend and frontend: #### 3.1 On the backend - - - + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - if (input.type === "PASSWORD_RESET") { - return originalImplementation.sendEmail({ - ...input, - passwordResetLink: input.passwordResetLink.replace( - // This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/reset-password` - "http://localhost:3000/auth/reset-password", - "http://localhost:3000/your/path" - ) - }) - } - return originalImplementation.sendEmail(input); - } - } - } - } - // highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + if (input.type === "PASSWORD_RESET") { + return originalImplementation.sendEmail({ + ...input, + passwordResetLink: input.passwordResetLink.replace( + // This is: `/auth/reset-password` + "http://localhost:3000/auth/reset-password", + "http://localhost:3000/your/path", + ), + }); + } + return originalImplementation.sendEmail(input); + }, + }; + }, + }, + }), + ], }); ``` - - - + + ```go import ( "strings" @@ -250,13 +228,12 @@ func main() { RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - //highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { - // This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/reset-password` + // This is: `/auth/reset-password` input.PasswordReset.PasswordResetLink = strings.Replace( input.PasswordReset.PasswordResetLink, "http://localhost:3000/auth/reset-password", @@ -267,17 +244,15 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword @@ -287,20 +262,18 @@ from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConf def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email - # highlight-start async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: - # This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/reset-password` + # This is: `/auth/reset-password` template_vars.password_reset_link = template_vars.password_reset_link.replace( "http://localhost:3000/auth/reset-password", "http://localhost:3000/your/path") return await original_send_email(template_vars, user_context) - # highlight-end original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( email_delivery=EmailDeliveryConfig(override=custom_email_deliver) @@ -308,111 +281,93 @@ init( ] ) ``` - - - + + #### 3.2 On the frontend - - - - + + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - EmailPassword.init({ - //highlight-start - - // The user will be taken to the custom path when they click on forgot password. - getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - return "/custom-reset-password-path"; - }; - } - //highlight-end - }) - ] -}) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + // The user will be taken to the custom path when they click on forgot password. + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + return "/custom-reset-password-path"; + } + }, + }), + ], +}); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIEmailPassword.init({ - //highlight-start - - // The user will be taken to the custom path when they click on forgot password. - getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - return "/custom-reset-password-path"; - }; - } - //highlight-end - }) - ] -}) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + // The user will be taken to the custom path when they click on forgot password. + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + return "/custom-reset-password-path"; + } + }, + }), + ], +}); ``` + + - - - - - + + -:::caution -Not applicable since you do not use pre-built UI. +:::warning[Not applicable since you do not use pre-built UI.] ::: - + ## Generate a reset link manually You can use the backend SDK to generate the reset password link as shown below: - - - + + ```tsx import EmailPassword from "supertokens-node/recipe/emailpassword"; async function createResetPasswordLink(userId: string, email: string) { - const linkResponse = await EmailPassword.createResetPasswordLink("public", userId, email); + const linkResponse = await EmailPassword.createResetPasswordLink("public", userId, email); - if (linkResponse.status === "OK") { - console.log(linkResponse.link); - } else { - // user does not exist or is not an email password user - } + if (linkResponse.status === "OK") { + console.log(linkResponse.link); + } else { + // user does not exist or is not an email password user + } } ``` - - - - + + ```go import ( "fmt" @@ -437,13 +392,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.emailpassword.asyncio import create_reset_password_link @@ -455,9 +407,8 @@ async def create_link(user_id: str, email: str): else: print("user does not exist or is not an email password user") ``` - - - + + ```python from supertokens_python.recipe.emailpassword.syncio import create_reset_password_link @@ -469,15 +420,13 @@ def create_link(user_id: str, email: str): else: print("user does not exist or is not an email password user") ``` + + + + - - - - - - -:::info Multy-tenancy +:::info[Multy-tenancy] Notice that the first argument to the function call above is `"public"`. This refers to the default tenant ID used in SuperTokens. It means that the generated password reset link can only apply to users belonging to the `"public"` tenant. @@ -493,31 +442,26 @@ Finally, the generated link uses the configured `websiteDomain` from the `appInf By default, the password reset link's lifetime is 1 hour. You can change this via a core's configuration (time in milliseconds): - - - + + ```bash # Here we set the lifetime to 2 hours. docker run \ -p 3567:3567 \ - // highlight-next-line - -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ + -e EMAIL_VERIFICATION_TOKEN_LIFETIME=7200000 \ -d supertokens/supertokens- ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command email_verification_token_lifetime: 7200000 ``` - - - + + :::info @@ -529,10 +473,10 @@ email_verification_token_lifetime: 7200000 ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/enterprise/_blocks/create-tenant.mdx b/docs/authentication/enterprise/_blocks/create-tenant.mdx deleted file mode 100644 index 23502ebadb..0000000000 --- a/docs/authentication/enterprise/_blocks/create-tenant.mdx +++ /dev/null @@ -1,176 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function createNewTenant() { - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] - }); - - if (resp.createdNew) { - // Tenant created successfully - } else { - // Existing tenant's config was modified. - } -} -``` - -The snippet creates a new tenant with the id `"customer1"`. -It enables the email password, third party and passwordless login methods for this tenant. -You can also disable any of these by not including them in the `firstFactors` input. -If `firstFactors` is not specified, by default, the system does not enable any of the login methods. - -If you set `firstFactors` to `null` the SDK uses any of the login methods. - -The built-in Factor IDs available for `firstFactors` include: - -| Authentication Type | Factor ID | -|-------------------|-----------| -| Email password auth | `emailpassword` | -| Social login / enterprise SSO auth | `thirdparty` | -| Passwordless - Email OTP | `otp-email` | -| Passwordless - SMS OTP | `otp-phone` | -| Passwordless - Email magic link | `link-email` | -| Passwordless - SMS magic link | `link-phone` | - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" -) - -func main() { - tenantId := "customer1" - emailPasswordEnabled := true - thirdPartyEnabled := true - passwordlessEnabled := true - - // highlight-start - resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ - EmailPasswordEnabled: &emailPasswordEnabled, - ThirdPartyEnabled: &thirdPartyEnabled, - PasswordlessEnabled: &passwordlessEnabled, - }) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // new tenant was created - } else { - // existing tenant's config was modified. - } -} -``` - -The code snippet creates a new tenant with the id `"customer1"`. -It enables the email password, third party and passwordless login methods for this tenant. -You can also disable any of these by setting the corresponding field to `false`. - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate - -async def some_func(): - response = await create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( - first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] - )) - - if response.status != "OK": - print("Handle error") - elif response.created_new: - print("New tenant was created") - else: - print("Existing tenant's config was updated") -``` - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate - -def some_func(): - response = create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( - first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] - )) - - if response.status != "OK": - print("Handle error") - elif response.created_new: - print("New tenant was created") - else: - print("Existing tenant's config was updated") -``` - - - - -The code snippet creates a new tenant with the id `"customer1"`. -It enables the email password, third party and passwordless login methods for this tenant. -You can also disable any of these by setting the corresponding field to `false`. - - - - - -```bash -curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ ---header 'api-key: YOUR_API_KEY' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "tenantId": "customer1", - "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] -}' -``` - -The request includes the `appId` for which you need to create a new tenant. -If you are using the default (`"public"`) app, you can omit the `/appid-` part of the URL. - -The snippet creates a new tenant with the id `"customer1"`. -It enables the email password, third party and passwordless login methods for this tenant. -You can also disable any of these by not including them in the `firstFactors` input. -If `firstFactors` is not specified, by default, the system does not enable any of the login methods. - -The built-in Factor IDs available for `firstFactors` include: - -| Authentication Type | Factor ID | -|-------------------|-----------| -| Email password auth | `emailpassword` | -| Social login / enterprise SSO auth | `thirdparty` | -| Passwordless - Email OTP | `otp-email` | -| Passwordless - SMS OTP | `otp-phone` | -| Passwordless - Email magic link | `link-email` | -| Passwordless - SMS magic link | `link-phone` | - - - - - -Create Tenant - -Create a new tenant by clicking on the **Add Tenant** button and specify the tenant ID. - -All Login Methods Enabled - -Once you create the tenant, turn on the Login Methods as required for the tenant. In the above example, you turn on all the Login Methods. - - - - diff --git a/docs/authentication/enterprise/_blocks/get-tenant.mdx b/docs/authentication/enterprise/_blocks/get-tenant.mdx deleted file mode 100644 index febe702957..0000000000 --- a/docs/authentication/enterprise/_blocks/get-tenant.mdx +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function getTenant(tenantId: string) { - - // highlight-start - let resp = await Multitenancy.getTenant(tenantId); - // highlight-end - - if (resp === undefined) { - // tenant does not exist - } else { - let coreConfig = resp.coreConfig; - - let firstFactors = resp.firstFactors; - - let configuredThirdPartyProviders = resp.thirdParty.providers; - } -} -``` - - - - -```go -import ( - "fmt" - - "github.com/supertokens/supertokens-golang/recipe/multitenancy" -) - -func main() { - tenantId := "customer1" - - // highlight-start - tenant, err := multitenancy.GetTenant(tenantId) - // highlight-end - - if err != nil { - // handle error - } - if tenant == nil { - // tenant does not exist - } else { - isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled; - isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; - isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; - - if (isEmailPasswordLoginEnabled) { - // Tenant support email password login - } - - if (isThirdPartyLoginEnabled) { - // Tenant support third party login - configuredThirdPartyProviders := tenant.ThirdParty.Providers; - fmt.Println(configuredThirdPartyProviders); - } - - if (isPasswordlessLoginEnabled) { - // Tenant support passwordless login - } - } -} -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import get_tenant - -async def some_func(): - tenant = await get_tenant("customer1") - - if tenant is None: - print("tenant does not exist") - else: - core_config = tenant.core_config - first_factors = tenant.first_factors - providers = tenant.third_party_providers - - print(core_config) - print(first_factors) - print(providers) -``` - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import get_tenant - -tenant = get_tenant("customer1") - -if tenant is None: - print("tenant does not exist") -else: - core_config = tenant.core_config - first_factors = tenant.first_factors - providers = tenant.third_party_providers - - print(core_config) - print(first_factors) - print(providers) -``` - - - - - - - - -```bash -curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ ---header 'api-key: YOUR_API_KEY' \ ---header 'Content-Type: application/json' -``` - -Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). - -If the input tenant does not exist, you get back a `200` status code with the following JSON: - -```json -{"status": "TENANT_NOT_FOUND_ERROR"} -``` - -Otherwise you get a `200` status code with the following JSON output: - -```json -{ - "status": "OK", - "thirdParty": { - "providers": [...] - }, - "coreConfig": { - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2" - }, - "tenantId": "customer1", - "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] -} -``` - - - - - -The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. diff --git a/docs/authentication/enterprise/_blocks/paid-feature-banner.mdx b/docs/authentication/enterprise/_blocks/paid-feature-banner.mdx deleted file mode 100644 index fb93304c11..0000000000 --- a/docs/authentication/enterprise/_blocks/paid-feature-banner.mdx +++ /dev/null @@ -1,9 +0,0 @@ -:::warning Paid Feature - -This is a paid feature. - -For self hosted users, [Sign up](https://supertokens.com/auth) to get a license key and follow the instructions sent to you by email. Creation of tenants is free on the `dev` license key. - -This feature is already enabled for managed service users. Creation of additional tenant is free on the provided development environment. - -::: \ No newline at end of file diff --git a/docs/authentication/enterprise/_blocks/update-tenant-config.mdx b/docs/authentication/enterprise/_blocks/update-tenant-config.mdx deleted file mode 100644 index f9e5abfb75..0000000000 --- a/docs/authentication/enterprise/_blocks/update-tenant-config.mdx +++ /dev/null @@ -1,192 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function createNewTenant() { - - // highlight-start - let resp = await Multitenancy.createOrUpdateTenant("customer1", { - coreConfig: { - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2", - } - }); - // highlight-end - - if (resp.createdNew) { - // new tenant was created - } else { - // existing tenant's config was modified. - } -} -``` - -In the above example, the system assigns different values for certain configurations for `customer1` tenant. -All other configurations inherit from the base configuration. - -Notice the `postgresql_connection_uri`. -This allows you to achieve **data isolation on a tenant level**. -This configuration is not required. -If not provided, the database stores the tenant's information as specified in the core's configuration. -It is still a different user pool though. - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ - CoreConfig: map[string]interface{}{ - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2", - }, - }) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // new tenant was created - } else { - // existing tenant's config was modified. - } -} -``` - -In the above example, the system assigns different values for certain configurations for `customer1` tenant. -All other configurations inherit from the base configuration. - -Notice the `postgresql_connection_uri`. -This allows you to achieve **data isolation on a tenant level**. -This configuration is not required. -If not provided, the database stores the tenant's information as specified in the core's configuration. -It is still a different user pool though. - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( - core_config={ - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2", - }, - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("new tenant created") - else: - print("existing tenant's config was modified.") -``` - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant -from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate - -tenant_id = "customer1" -result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( - core_config={ - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2", - }, -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("new tenant created") -else: - print("existing tenant's config was modified.") -``` - - - - -In the above example, the system assigns different values for certain configurations for `customer1` tenant. -All other configurations inherit from the base configuration. - -Notice the `postgresql_connection_uri`. -This allows you to achieve **data isolation on a tenant level**. -This configuration is not required. -If not provided, the database stores the tenant's information as specified in the core's configuration. -It is still a different user pool though. - - - - - - -```bash -curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ ---header 'api-key: YOUR_API_KEY' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "tenantId": "customer1", - "coreConfig": { - "email_verification_token_lifetime": 7200000, - "password_reset_token_lifetime": 3600000, - "postgresql_connection_uri": "postgresql://localhost:5432/db2" - } -}' -``` - -In the above example, the system assigns different values for certain configurations for `customer1` tenant. -All other configurations inherit from the base configuration. - -Notice the `postgresql_connection_uri`. -This allows you to achieve **data isolation on a tenant level**. -This configuration is not required. -If not provided, the database stores the tenant's information as specified in the core's configuration. -It is still a different user pool though. - - - - - -Custom tenant configuration - - -In the above example, the system assigns different values for certain configurations for `customer1` tenant. -All other configurations inherit from the base configuration. -You can edit the values by clicking on the pencil icon and then specifying a new value. - -:::caution - -You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them. - -::: - - - - diff --git a/docs/authentication/enterprise/_category_.json b/docs/authentication/enterprise/_category_.json deleted file mode 100644 index aa8107cd25..0000000000 --- a/docs/authentication/enterprise/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Enterprise Login", - "position": 5 -} diff --git a/docs/authentication/enterprise/common-domain-login.mdx b/docs/authentication/enterprise/common-domain-login.mdx index 13f5eecbb3..3d5d09693f 100644 --- a/docs/authentication/enterprise/common-domain-login.mdx +++ b/docs/authentication/enterprise/common-domain-login.mdx @@ -1,25 +1,19 @@ --- title: Implement common domain login -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 3 description: Authenticate users across different tenants through a common domain. -page_type: guide -recipe: thirdparty -category: enterprise-login +sidebar: + order: 40 --- -# Common domain login +## Overview -## Overview - -This guide shows you how to authenticate users through the same page, `https://example.com/auth`, and then redirect to their sub domain after login. +This guide shows you how to authenticate users through the same page, `https://example.com/auth`, and then redirect them to their subdomain after sign-in. The login page adjusts the authentication method based on the tenant's configuration. -You can figure out which is the tenant that you are working with through different methods. -A common way is to ask the user to enter their organisation name, which is equal to the `tenantId` that you configure in SuperTokens. +You can determine the tenant in several ways. +A common approach is to ask the user for their organization name and use it as the `tenantId` configured in SuperTokens. -:::info Important +:::info[Important] You can find an example app for this setup with the **pre-built UI** on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-many-subdomains). The app is setup to have three tenants: - `tenant1`: Login with `emailpassword` + Google sign in - `tenant2`: Login with `emailpassword` @@ -39,438 +33,398 @@ npx create-supertokens-app@latest --recipe=multitenancy The tutorial assumes that you already have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +If you have not, please check the [Quickstart Guide](/quickstart). You also need to create the tenants that your application requires. -View the [previous tutorial](/docs/authentication/enterprise/initial-setup) for more information on how to do this. +View the [previous tutorial](/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps ### 1. Ask for the tenant ID on the login page - + - + - -If you have [followed the pre-built UI setup](/docs/quickstart/frontend-setup), when you visit the login screen, you see the login screen immediately. +If you have [followed the pre-built UI setup](/quickstart#1-integrate-the-frontend-sdk), when you visit the login screen, you see the login screen immediately. The flow needs to change to first ask the user to enter their tenant ID and then display the login UI based on the tenant ID. To do that, first obtain the tenant ID from the user. -You can achieve this by building a UI that asks them to enter their `tenantId` or Organisation name (which can serve as the tenant ID). -This UI builds in a component called `AuthPage`. +You can achieve this by building a UI that asks them to enter their tenant ID or organization name (which can serve as the tenant ID). +This example implements the UI in a component called `AuthPage`. -:::warning no-title -You have to [create tenants](/docs/authentication/enterprise/initial-setup) before you can complete this step. +:::warning +You have to [create tenants](/authentication/enterprise/initial-setup) before you can complete this step. ::: - - - - - + + +:::info[Caution] +No code snippet provided here, however, if you visit the auth component, you see that the pre-built UI renders in the `"supertokensui"` `div` element on page load. The logic here needs to change to first check if the user has provided the `tenantId`. If they have, the SuperTokens UI renders as usual. If they have not, a simple UI renders which asks the user for their tenant id and saves that in `localstorage`. - +Switch to the React code tab here to see the implementation in React, and a similar logic applies here. +::: + + + + + + ```tsx import { useState } from "react"; import * as reactRouterDom from "react-router-dom"; import { Routes } from "react-router-dom"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; export const AuthPage = () => { - const location = reactRouterDom.useLocation(); - const [inputTenantId, setInputTenantId] = useState(""); - const tenantId = localStorage.getItem("tenantId") ?? undefined; - const session = useSessionContext(); - - if (session.loading) { - return null; - } - - if ( - tenantId !== undefined || // if we have a tenantId stored - session.doesSessionExist === true || // or an active session (it'll contain the tenantId) - new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId - ) { - // Since this component (AuthPage) is rendered in the ^{appInfo.websiteBasePath} route in the main Routes component, - // and we are rendering this in a sub route as shown below, the third arg to getSuperTokensRoutesForReactRouterDom - // tells SuperTokens to create Routes without ^{appInfo.websiteBasePath} prefix to them, otherwise they would - // render on ^{appInfo.websiteBasePath}^{form_websiteBasePath} path. - return ( - - {getSuperTokensRoutesForReactRouterDom( - reactRouterDom, - [EmailPasswordPreBuiltUI], - "^{appInfo.websiteBasePath}" - )} - - ); - } else { - return ( -
{ - // this value will be read by SuperTokens as shown in the next steps. - localStorage.setItem("tenantId", inputTenantId); - }}> -

Enter your organisation's name:

- setInputTenantId(e.target.value)} /> -
- -
- ); - } + const location = reactRouterDom.useLocation(); + const [inputTenantId, setInputTenantId] = useState(""); + const tenantId = localStorage.getItem("tenantId") ?? undefined; + const session = useSessionContext(); + + if (session.loading) { + return null; + } + + if ( + tenantId !== undefined || // if we have a tenantId stored + session.doesSessionExist === true || // or an active session (it'll contain the tenantId) + new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId + ) { + // Since this component (AuthPage) is rendered in the /auth route in the main Routes component, + // and we are rendering this in a sub route as shown below, the third arg to getSuperTokensRoutesForReactRouterDom + // tells SuperTokens to create Routes without /auth prefix to them, otherwise they would + // render on /auth path. + return {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI], "/auth")}; + } else { + return ( +
{ + // this value will be read by SuperTokens as shown in the next steps. + localStorage.setItem("tenantId", inputTenantId); + }} + > +

Enter your organization's name:

+ setInputTenantId(e.target.value)} /> +
+ +
+ ); + } }; ``` - - - - +
+ ```tsx import { useState } from "react"; import { getRoutingComponent } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import { useSessionContext } from "supertokens-auth-react/recipe/session"; export const AuthPage = () => { - const [inputTenantId, setInputTenantId] = useState(""); - const tenantId = localStorage.getItem("tenantId") ?? undefined; - const session = useSessionContext(); - - if (session.loading) { - return null; - } - - if ( - tenantId !== undefined || // if we have a tenantId stored - session.doesSessionExist === true || // or an active session (it'll contain the tenantId) - new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId - ) { - return getRoutingComponent([EmailPasswordPreBuiltUI]); - } else { - return ( -
{ - // this value will be read by SuperTokens as shown in the next steps. - localStorage.setItem("tenantId", inputTenantId); - }}> -

Enter your organisation's name:

- setInputTenantId(e.target.value)} /> -
- -
- ); - } + const [inputTenantId, setInputTenantId] = useState(""); + const tenantId = localStorage.getItem("tenantId") ?? undefined; + const session = useSessionContext(); + + if (session.loading) { + return null; + } + + if ( + tenantId !== undefined || // if we have a tenantId stored + session.doesSessionExist === true || // or an active session (it'll contain the tenantId) + new URLSearchParams(location.search).has("tenantId") // or we are on a link (e.g.: email verification) that contains the tenantId + ) { + return getRoutingComponent([EmailPasswordPreBuiltUI]); + } else { + return ( +
{ + // this value will be read by SuperTokens as shown in the next steps. + localStorage.setItem("tenantId", inputTenantId); + }} + > +

Enter your organization's name:

+ setInputTenantId(e.target.value)} /> +
+ +
+ ); + } }; ``` +
+
+
+ - -
+ + -The example creates a simple UI renders which asks the user for their organisation's name. + + +The example creates a simple UI that asks the user for their organization's name. Their input serves as their tenant ID. -When the user submits that form, the value stores in `localstorage`. +When the user submits that form, the value is stored in local storage. -:::info Important -The `AuthPage` component should render to show on `^{appInfo.websiteBasePath}/*` paths of the website. +:::info[Important] +The `AuthPage` component should render to show on `/auth/*` paths of the website. The `AuthPage` replaces the call to `getSuperTokensRoutesForReactRouterDom` or `getRoutingComponent` that you may have added to your app from the quick setup section. ::: + + -
- - - -:::info Caution -No code snippet provided here, however, if you visit the auth component, you see that the pre-built UI renders in the `"supertokensui"` `div` element on page load. The logic here needs to change to first check if the user has provided the `tenantId`. If they have, the SuperTokens UI renders as usual. If they have not, a simple UI renders which asks the user for their tenant id and saves that in `localstorage`. - -Switch to the React code tab here to see the implementation in React, and a similar logic applies here. -::: - - - -
+
-
+ - -You need to build a UI that asks the user to enter their `tenantId` or Organisation name (which can serve as the tenant ID). The input value is then utilized in function calls, as seen below. +You need to build a UI that asks the user to enter their tenant ID or organization name (which can serve as the tenant ID). The input value is then used in function calls, as shown below. Once you have the user's tenant ID, you can fetch their list of configured providers and render the third party login buttons accordingly: - - - - - + + + + ```tsx -import Multitenancy from 'supertokens-web-js/recipe/multitenancy'; +import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { - const loginMethods = await Multitenancy.getLoginMethods({ - tenantId - }) - - if (loginMethods.firstFactors.includes("thirdparty")) { - const providers = loginMethods.thirdParty.providers; - if (providers.find(i => i.id === "active-directory")) { - // render sign in with Active Directory button - } else { - // more checks for other providers - } - } else { - // thirdparty login is disabled for the tenant - } + const loginMethods = await Multitenancy.getLoginMethods({ + tenantId, + }); + + if (loginMethods.firstFactors.includes("thirdparty")) { + const providers = loginMethods.thirdParty.providers; + if (providers.find((i) => i.id === "active-directory")) { + // render sign in with Active Directory button + } else { + // more checks for other providers + } + } else { + // thirdparty login is disabled for the tenant + } } ``` - - - - + + ```tsx -import Multitenancy from 'supertokens-web-js/recipe/multitenancy'; +import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { - const loginMethods = await Multitenancy.getLoginMethods({ - tenantId - }) - - if (loginMethods.firstFactors.includes("thirdparty")) { - const providers = loginMethods.thirdParty.providers; - if (providers.find(i => i.id === "active-directory")) { - // render sign in with Active Directory button - } else { - // more checks for other providers - } - } else { - // thirdparty login is disabled for the tenant - } + const loginMethods = await Multitenancy.getLoginMethods({ + tenantId, + }); + + if (loginMethods.firstFactors.includes("thirdparty")) { + const providers = loginMethods.thirdParty.providers; + if (providers.find((i) => i.id === "active-directory")) { + // render sign in with Active Directory button + } else { + // more checks for other providers + } + } else { + // thirdparty login is disabled for the tenant + } } ``` - - - - -- In the code snippet above, the login methods for the `tenantId` fetch. -- We then render the login UI buttons based on the configured `thirdPartyId`s in the response - - - - - - - + + + + ```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/loginmethods' +curl --location --request GET '/auth/loginmethods' ``` - + + + + + +- The code snippet fetches the login methods for the tenant ID. +- It then renders the login UI buttons based on the configured `thirdPartyId` values in the response. + + The response body from the API call has a `status` property in it: + - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should appear on the frontend. + + - - - + -### 2. Include the tenant ID in authentication flow +### 2. Include the tenant ID in authentication flow You need to tell SuperTokens how to resolve the tenant ID. To do this, set the `getTenantId` function in the `Multitenancy` recipe. In the current example, local storage provides the `tenantId`. - - + - - - + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - apiBasePath: "...", - websiteBasePath: "..." - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - // highlight-start - Multitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: (input) => { - let tid = localStorage.getItem("tenantId"); - return tid === null ? undefined : tid; - } - } - } - } - }) - // highlight-end - // other recipes... - ] + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + apiBasePath: "...", + websiteBasePath: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + let tid = localStorage.getItem("tenantId"); + return tid === null ? undefined : tid; + }, + }; + }, + }, + }), + // other recipes... + ], }); ``` - -:::info Important -Set the `usesDynamicLoginMethods` to `true` to tell SuperTokens that the login methods are dynamic (based on the `tenantId`). On page load (of the login page), SuperTokens first fetches the configured login methods for the `tenantId`. It then displays the login UI based on the result of the API call. -::: - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy"; supertokensUIInit({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - apiBasePath: "...", - websiteBasePath: "..." - }, - // highlight-next-line - usesDynamicLoginMethods: true, - recipeList: [ - // highlight-start - supertokensUIMultitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: (input) => { - let tid = localStorage.getItem("tenantId"); - return tid === null ? undefined : tid; - } - } - } - } - }) - // highlight-end - // other recipes... - ] + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + apiBasePath: "...", + websiteBasePath: "...", + }, + usesDynamicLoginMethods: true, + recipeList: [ + supertokensUIMultitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + let tid = localStorage.getItem("tenantId"); + return tid === null ? undefined : tid; + }, + }; + }, + }, + }), + // other recipes... + ], }); ``` + + -:::info Important +:::info[Important] Set the `usesDynamicLoginMethods` to `true` to tell SuperTokens that the login methods are dynamic (based on the `tenantId`). On page load (of the login page), SuperTokens first fetches the configured login methods for the `tenantId`. It then displays the login UI based on the result of the API call. ::: - - - + - + - - - -Initialize the multi tenancy recipe with the following callback defined. You can get the value of `tenantId` from wherever you had stored it when asking the user for its value. - - - + + +Initialize the multitenancy recipe with the following callback. You can get the tenant ID from wherever you stored it after asking the user for it. + + +All the steps for mobile app login are similar to the [social login steps](/authentication/social/initial-setup#2-add-the-login-ui). However, when you are calling the sign in up API, you also need to pass in the `tenantId` in the request path. An example of this appears below: + + + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - }, - recipeList: [ - // highlight-start - Multitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: (input) => { - let tid = localStorage.getItem("tenantId"); - return tid === null ? undefined : tid; - } - } - } - } - }) - // highlight-end - // other recipes... - ] + appInfo: { + appName: "...", + apiDomain: "...", + }, + recipeList: [ + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + let tid = localStorage.getItem("tenantId"); + return tid === null ? undefined : tid; + }, + }; + }, + }, + }), + // other recipes... + ], }); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultitenancy from "supertokens-web-js-script/recipe/multitenancy"; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - }, - recipeList: [ - // highlight-start - supertokensMultitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: (input) => { - let tid = localStorage.getItem("tenantId"); - return tid === null ? undefined : tid; - } - } - } - } - }) - // highlight-end - // other recipes... - ] + appInfo: { + appName: "...", + apiDomain: "...", + }, + recipeList: [ + supertokensMultitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: (input) => { + let tid = localStorage.getItem("tenantId"); + return tid === null ? undefined : tid; + }, + }; + }, + }, + }), + // other recipes... + ], }); ``` - - - - - - - - -All the steps for mobile app login are similar to the [social login steps](../../custom-ui/thirdparty-login). However, when you are calling the sign in up API, you also need to pass in the `tenantId` in the request path. An example of this appears below: - - - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ +curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", @@ -481,55 +435,49 @@ curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signi }, }' ``` + + - - - + -### 3. Redirect users based on tenant subdomain {{optional}} +### 3. Redirect users based on tenant subdomain (optional) -If each tenant has access to specific sub domains in your application you need to redirect them after login. +If each tenant has access to specific subdomains in your application, redirect users after sign-in. #### 3.1 Restrict subdomain access -Before you perform the actual redirect step, you should restrict which sub domains they have access to. +Before redirecting users, restrict which subdomains their sessions can be used on. To do this configure the SDK to know which domain each `tenantId` has access to. - - - - + + ```tsx import SuperTokens from "supertokens-node"; -import Multitenancy from "supertokens-node/recipe/multitenancy" +import Multitenancy from "supertokens-node/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - Multitenancy.init({ - getAllowedDomainsForTenantId: async (tenantId, userContext) => { - // query your db to get the allowed domain for the input tenantId - // or you can make the tenantId equal to the sub domain itself - return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"] - } - }), - // highlight-end - // other recipes... - ] -}) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Multitenancy.init({ + getAllowedDomainsForTenantId: async (tenantId, userContext) => { + // query your db to get the allowed domain for the input tenantId + // or you can make the tenantId equal to the subdomain itself + return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]; + }, + }), + // other recipes... + ], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -540,23 +488,19 @@ import ( func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ - // highlight-start multitenancy.Init(&multitenancymodels.TypeInput{ GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext) ([]string, error) { // query your db to get the allowed domain for the input tenantId - // or you can make the tenantId equal to the sub domain itself + // or you can make the tenantId equal to the subdomain itself return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil }, }), - // highlight-end }, }) } ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multitenancy @@ -582,351 +526,344 @@ init( ], ) ``` + + + +The code sample tells SuperTokens to add the returned domains to the user's session claims when they sign in. +The claim is available on the frontend and backend and can restrict where the session is used. + +:::warning[Domain checks are not tenant authorization] +`AllowedDomainsClaim` and `hasAccessToCurrentDomain` restrict session use by hostname. They do not prove that the user +belongs to an organization, enforce CORS or allowed browser origins, authorize access to business data, or provide +complete tenant isolation. After authentication, verify the user's tenant membership. On every business-data access, +the backend must derive the tenant from trusted session data and enforce application-level tenant authorization. Do +not trust a tenant ID, hostname, or claim supplied by the browser as authorization. +::: - - - - -The code sample tells SuperTokens to add the list of domains into the user's session claims once they login. -This claim then becomes accessible on the frontend and backend to restrict user's access to the right domain(s). - -#### 3.2 Redirect the user to their sub domain after sign in - - - -On the frontend side, post sign in, by default, the frontend SDK redirects the user to the `/` route. -Can change this to instead redirect them to their sub domain based on their `tenantId`. +#### 3.2 Redirect the user to their subdomain after sign-in - + - +After sign-in, the frontend SDK redirects the user to the `/` route by default. +You can instead redirect them to their subdomain based on their tenant ID. + + ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - }, - // highlight-end - recipeList: [ /* Recipe init here... */ ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim, + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default subdomain + } + } + return undefined; + }, + recipeList: [ + /* Recipe init here... */ + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - let claimValue: string[] | undefined = await supertokensUISession.getClaimValue({ - claim: supertokensUIMultitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } - } - return undefined; - }, - // highlight-end - recipeList: [ /* Recipe init here... */ ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + let claimValue: string[] | undefined = await supertokensUISession.getClaimValue({ + claim: supertokensUIMultitenancy.AllowedDomainsClaim, + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; + } else { + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default subdomain + } + } + return undefined; + }, + recipeList: [ + /* Recipe init here... */ + ], }); ``` + + - - - - - - - + -On the frontend, once the user has completed login, you can read the domain that they have access to (from their session) and redirect them accordingly. + - - +On the frontend, after the user signs in, you can read the domain from their session and redirect them accordingly. + + ```tsx import Session from "supertokens-web-js/recipe/session"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function redirectToSubDomain() { - if (await Session.doesSessionExist()) { - let claimValue: string[] | undefined = await Session.getClaimValue({ - claim: Multitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } + if (await Session.doesSessionExist()) { + let claimValue: string[] | undefined = await Session.getClaimValue({ + claim: Multitenancy.AllowedDomainsClaim, + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; } else { - window.location.href = "/auth"; + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default subdomain } + } else { + window.location.href = "/auth"; + } } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensMultitenancy from "supertokens-web-js-script/recipe/multitenancy"; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." async function redirectToSubDomain() { - if (await supertokensSession.doesSessionExist()) { - let claimValue: string[] | undefined = await supertokensSession.getClaimValue({ - claim: supertokensMultitenancy.AllowedDomainsClaim - }); - if (claimValue !== undefined) { - window.location.href = "https://" + claimValue[0]; - } else { - // there was no configured allowed domain for this user. Throw an error cause of - // misconfig or redirect to a default sub domain - } + if (await supertokensSession.doesSessionExist()) { + let claimValue: string[] | undefined = await supertokensSession.getClaimValue({ + claim: supertokensMultitenancy.AllowedDomainsClaim, + }); + if (claimValue !== undefined) { + window.location.href = "https://" + claimValue[0]; } else { - window.location.href = "/auth"; + // there was no configured allowed domain for this user. Throw an error cause of + // misconfig or redirect to a default subdomain } + } else { + window.location.href = "/auth"; + } } ``` + + - - - - + - The `AllowedDomainsClaim` claim is auto added to the session by the backend SDK if you provide the `GetAllowedDomainsForTenantId` configuration from the previous step. -- This claim contains a list of domains that this user can access, based on their tenant ID. +- This claim contains the domains configured for the session's tenant ID. It is not proof of user membership or authorization to business data. -### 6. Share sessions across sub domains {{optional}} +### 6. Share sessions across subdomains (optional) -If the user authenticates into your main website domain (`https://example.com/auth`), and redirects to a sub domain, the session recipe needs updating. -It should allow sharing of sessions across sub domains. -You can do this [by setting the `sessionTokenFrontendDomain` value in the Session recipe](/docs/post-authentication/session-management/share-session-across-sub-domains). +If the user authenticates on your main website domain (`https://example.com/auth`) and is redirected to a subdomain, update the Session recipe to share sessions across subdomains. +You can do this [by setting the `sessionTokenFrontendDomain` value in the Session recipe](/post-authentication/session-management/share-session-across-sub-domains). -If the sub domains assigned to your tenants have their own backend on a separate sub domain (one per tenant), you can also enable [sharing of sessions across API domains](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). +If the subdomains assigned to your tenants have their own backends on separate subdomains (one per tenant), you can also enable [sharing of sessions across API domains](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). -### 7. Limit user access to their sub domain. {{optional}} +### 7. Limit session use to the tenant's subdomain (optional) -The frontend uses session claim validators to restrict sub domain access. +The frontend uses session claim validators to restrict session use by subdomain. Before proceeding, make sure that you define the `GetAllowedDomainsForTenantId` function mentioned above. This adds the list of allowed domains into the user's access token payload. -On the frontend, it is necessary to check if the tenant has access to the current sub domain. -If not, they should redirect to the right sub domain. -You can achieve this by using the `hasAccessToCurrentDomain` session validator from the multi tenancy recipe. +On the frontend, check whether the current subdomain is in the session's allowed domains. +If it is not, redirect the user to the correct subdomain. +You can achieve this by using the `hasAccessToCurrentDomain` session validator from the multitenancy recipe. - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import React from "react"; -import Session from 'supertokens-auth-react/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-auth-react/recipe/multitenancy'; +import Session from "supertokens-auth-react/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-auth-react/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/docs/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -import supertokensMultitenancy from "supertokens-web-js-script/recipe/multitenancy"; supertokensUISession.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await supertokensUISession.getClaimValue({ - claim: supertokensMultitenancy.AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await supertokensUISession.getClaimValue({ + claim: supertokensMultitenancy.AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` - + + + + + +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy'; +import Session from "supertokens-web-js/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` + + -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](../sessions/protecting-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). - - - - - - + + +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. + + - + - - + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy'; +import Session from "supertokens-web-js/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` - - - - -```tsx -import supertokensSession from 'supertokens-web-js-script/recipe/session'; -import supertokensMultitenancy from 'supertokens-web-js-script/recipe/multitenancy'; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokensSession.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await supertokensSession.getClaimValue({ - claim: supertokensMultitenancy.AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await supertokensSession.getClaimValue({ + claim: supertokensMultitenancy.AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` + + - - - -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](../sessions/protecting-frontend-routes#verifying-the-claims-of-a-session--cust), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes#check-the-claims-of-a-session), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. - + --- ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/enterprise/important-concepts.mdx b/docs/authentication/enterprise/important-concepts.mdx index 910b6ac402..cacc9261e0 100644 --- a/docs/authentication/enterprise/important-concepts.mdx +++ b/docs/authentication/enterprise/important-concepts.mdx @@ -1,43 +1,47 @@ --- -hide_title: true -sidebar_position: 2 title: Important concepts description: Understand multi-tenant architecture with SuperTokens -page_type: guide -recipe: thirdparty -category: enterprise-login +sidebar: + order: 20 --- -# Important concepts - ## Overview -[Multitenancy](https://supertokens.com/features/multi-tenancy) is a way to organize your application to support multiple groups of users who share a common access with to an application. -The user groups, or **tenants**, isolate from one another and can only access their specific data. -Furthermore, each tenant can have different methods of logging in, configured by the tenant, or by you (the application developer). +[Multitenancy](https://supertokens.com/features/multi-tenancy) organizes an application into groups of users that share access to it. +These groups, or **tenants**, can have separate authentication configuration and user pools. +Each tenant can also have different sign-in methods, configured by the tenant or by you, the application developer. For example, a SaaS application for a financial company may want to separate their users by the financial institution they represent. -This would require a login screen that asks for a username and password, as well as the name of the tenant. +This might use a sign-in screen that asks for a username, password, and tenant name. The application would then route the user to their specific tenant, which could be a different database or a different collection of data within a database. ## References -With SuperTokens, there are two levels of abstractions for multi tenancy: **Tenant** and **Application**. +With SuperTokens, there are two levels of abstraction for multitenancy: **Tenant** and **Application**. + +:::warning[Authentication tenancy is not complete data isolation] +SuperTokens tenants separate authentication configuration and can separate authentication user pools. A tenant ID in +an authentication request or session does not prove organization membership or authorize access to application data. +After authentication, verify the user's membership in the tenant. On every business-data access, derive the tenant from +trusted session data and enforce application-level tenant authorization. Scope database queries and storage to that +authorized tenant. Do not rely on tenant routing, hostnames, CORS, frontend checks, or session claims alone for tenant +isolation. +::: ### Tenant -A tenant is a group of users who share a common access with specific privileges to the application. Key characteristics: +A tenant is a group of users with shared access and specific privileges in an application. Key characteristics: -- Each tenant can have its own login method. -For example, one tenant can have email password login, while another can have SSO login. +- Each tenant can have its own sign-in method. +For example, one tenant can use email-password sign-in, while another can use SSO. - Each tenant has its own user pool. -One user can login using the same email across different tenants, and the system treats them as different users. +One user can sign in with the same email across different tenants, and the system treats the identities as different users. You can also share a user across tenants. -- This feature also allows implementing data isolation for each of your tenants wherein a different database serves each tenant. -- Roles and permissions exist on an app level, but the system defines their mapping to the users on a tenant level. +- You can isolate SuperTokens authentication data further by using a different database for each tenant. Business-data isolation remains the application's responsibility. +- Roles and permissions exist at the application level, but their mapping to users is defined at the tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. -It also means that you can share the same role / permission set across tenants. +It also means that you can share the same role and permission set across tenants. - Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. - For multiple tenants, you can run the same backend and frontend across all tenants of an app. Each request from the frontend contains a `tenantId` identifying that tenant to the backend, and once logged in, each session also contains that user's `tenantId`. @@ -48,55 +52,71 @@ The top-most level of abstraction in SuperTokens multitenancy. Key characteristi - Each app can have its own set of tenants and users, which can't be shared with other apps. - Each app needs to have its own SuperTokens backend and SuperTokens frontend SDK setup. -- User metadata is per app level since users are also on a per app level. +- User metadata is at the application level because users are also at the application level. - When you start the core for the first time, SuperTokens creates an app (`appId` is `"public"`) and one tenant in it (tenantId is `"public"`). When you create a new app, you also get a new tenant (`tenantId` is `"public"`) as part of that app created for you. - A user can be uniquely recognized by their `appId` -> `userId`. -This allows the same user to share across tenants (if you want that to happen). +This allows the same user to be shared across tenants if required. - The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. -This allows the same email to use across tenants in a way that they are still treated as different users (they have different user IDs). -The same holds true for phone numbers and third party login profile. +This allows the same email to be used across tenants while still being treated as different users with different user IDs. +The same applies to phone numbers and third-party sign-in profiles. - You can create multiple apps and tenants in the same database or in different databases. The only restriction is that for an app, you cannot share a user across `tenantA` and `tenantB` if the databases for `tenantA` and `tenantB` are different. -Putting it another way, a user can only share across the tenants if those tenants use the same database. +In other words, a user can only be shared across tenants that use the same database. ## Types of setup -Based on the above abstractions you can choose between four different setup types when setting up authentication with **SuperTokens**. +Based on these abstractions, you can choose from four setup types when configuring authentication with **SuperTokens**. ### Single tenant, single app -The default use case when you are not making use of the multi tenancy feature. +The default use case when you are not using the multitenancy feature. -![Single tenant single app architecture](/img/single-tenant-single-app.png) +Single tenant single app architecture ### Single tenant, multi app This is where you have multiple applications running on the same SuperTokens core instance and each application has a single user pool. This could be two different apps in your organization, or two different development environments for the same app (or some combination of this). -![Single tenant multi app architecture](/img/single-tenant-multi-app.png) +Single tenant multi app architecture ### Multi tenant, single app Different customers use the same application, but each customer has their own set of users and login methods (each customer is a unique tenant in SuperTokens). -![Multi tenant single app architecture](/img/multi-tenant-single-app.png) +Multi tenant single app architecture ### Multi tenant, multi app Multiple applications run on the same **SuperTokens** core instance, and each application has its own set of tenants. This could be two different applications in your organization, or two different development environments for the same application (or some combination of this). -![Multi tenant multi app architecture](/img/multi-tenant-multi-app.png) +Multi tenant multi app architecture In a multi app, multi tenant setup: A user can be uniquely recognized by their `appId` -> `userId`. -This allows the same user to share across tenants (if you want that to happen). +This allows the same user to be shared across tenants if required. The identity of the user (their email for example) can be uniquely identified by `appId` -> `tenantId` -> email. -This allows the same email to use across tenants in a way that they are still treated as different users (they have different user IDs). -The same holds true for phone numbers and third party login profile. -Roles and permissions exist on an app level, but the system defines their mapping to the users on a tenant level. +This allows the same email to be used across tenants while still being treated as different users with different user IDs. +The same applies to phone numbers and third-party sign-in profiles. +Roles and permissions exist at the application level, but their mapping to users is defined at the tenant level. This means that the same user shared across tenants can have different roles / permissions, depending on the tenant they log into. -It also means that you can share the same role / permission set across tenants. +It also means that you can share the same role and permission set across tenants. Sessions are per tenant (`appId` -> `tenantId` -> session handle) and cannot be shared across tenants. -User metadata is per app level since users are also on a per app level. +User metadata is at the application level because users are also at the application level. diff --git a/docs/authentication/enterprise/initial-setup.mdx b/docs/authentication/enterprise/initial-setup.mdx index a52456a371..f280f3ec9f 100644 --- a/docs/authentication/enterprise/initial-setup.mdx +++ b/docs/authentication/enterprise/initial-setup.mdx @@ -1,33 +1,25 @@ --- title: Initial setup -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 description: Create your first tenant and configure authentication on it. -page_type: tutorial -recipe: thirdparty -category: enterprise-login +sidebar: + label: Initial Setup + order: 30 --- - -import CreateTenantBlock from "./_blocks/create-tenant.mdx"; -import UpdateTenantConfigBlock from "./_blocks/update-tenant-config.mdx"; -import GetTenantBlock from "./_blocks/get-tenant.mdx"; - -import AddProviderConfigurationBlock from "../social/_blocks/multi-tenant-provider-config-active-directory.mdx" - -# Initial setup + +Set up SuperTokens multi-tenancy for this application. Inspect the existing backend, frontend, authentication recipes, and tenant identification strategy. Determine whether the feature requires the managed service, configure tenant creation and enabled first factors, and add enterprise provider configuration with credentials stored in environment variables. Preserve existing conventions, avoid committing secrets, and validate tenant resolution, provider callbacks, login, and session behavior for more than one tenant. + ## Before you start -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page please follow the tutorial and return here once you're done. - ## Steps -### 1. Create a tenant +### 1. Create a tenant The first step in setting up a multi tenant login system is to create a tenant in the SuperTokens core. Each tenant has a unique `tenantId` mapped to that tenant's configuation. @@ -35,44 +27,626 @@ The `tenantId` could be that tenant's sub domain, or a workspace URL, or anythin The configuration mapped to each tenant contains information about which login methods they enable. - + + +Create Tenant + +Create a new tenant by clicking on the **Add Tenant** button and specify the tenant ID. + +All Login Methods Enabled + +Once you create the tenant, turn on the Login Methods as required for the tenant. In the above example, you turn on all the Login Methods. + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" +) + +func main() { + tenantId := "customer1" + emailPasswordEnabled := true + thirdPartyEnabled := true + passwordlessEnabled := true + + resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ + EmailPasswordEnabled: &emailPasswordEnabled, + ThirdPartyEnabled: &thirdPartyEnabled, + PasswordlessEnabled: &passwordlessEnabled, + }) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +async def some_func(): + response = await create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( + first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] + )) + + if response.status != "OK": + print("Handle error") + elif response.created_new: + print("New tenant was created") + else: + print("Existing tenant's config was updated") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +def some_func(): + response = create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( + first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] + )) + + if response.status != "OK": + print("Handle error") + elif response.created_new: + print("New tenant was created") + else: + print("Existing tenant's config was updated") +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] +}' +``` + + + + + + + + +The snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by not including them in the `firstFactors` input. +If `firstFactors` is not specified, by default, the system does not enable any of the login methods. -#### Configure third party providers +If you set `firstFactors` to `null` the SDK uses any of the login methods. + +The built-in Factor IDs available for `firstFactors` include: + +| Authentication Type | Factor ID | +|-------------------|-----------| +| Email password auth | `emailpassword` | +| Social login / enterprise SSO auth | `thirdparty` | +| Passwordless - Email OTP | `otp-email` | +| Passwordless - SMS OTP | `otp-phone` | +| Passwordless - Email magic link | `link-email` | +| Passwordless - SMS magic link | `link-phone` | + + +The code snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by setting the corresponding field to `false`. + + +The code snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by setting the corresponding field to `false`. + + +The request includes the `appId` for which you need to create a new tenant. +If you are using the default (`"public"`) app, you can omit the `/appid-` part of the URL. + +The snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by not including them in the `firstFactors` input. +If `firstFactors` is not specified, by default, the system does not enable any of the login methods. + +The built-in Factor IDs available for `firstFactors` include: + +| Authentication Type | Factor ID | +|-------------------|-----------| +| Email password auth | `emailpassword` | +| Social login / enterprise SSO auth | `thirdparty` | +| Passwordless - Email OTP | `otp-email` | +| Passwordless - SMS OTP | `otp-phone` | +| Passwordless - Email magic link | `link-email` | +| Passwordless - SMS magic link | `link-phone` | + + + +#### Configure third party providers If you are using the `thirdparty` recipe on a tenant, you also need to set the providers that you want to use with it. -There's an extensive list of [built-in providers](/docs/authentication/social/built-in-providers-config), but you can also configure [a custom provider](/docs/authentication/enterprise/manage-tenants). +There's an extensive list of [built-in providers](/authentication/social/built-in-providers-config), but you can also configure [a custom provider](/authentication/enterprise/manage-tenants). The next code snippet shows how you can add an Active Directory login to your tenant. Update the `clientId`, `clientSecret`, and `directoryId` based on your tenant configuration. - + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "active-directory", + name: "Active Directory", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "active-directory", + Name: "Active Directory", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="active-directory", + name="Active Directoy", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + + + + ### 2. Provide additional configuration per tenant -You can also configure a tenant to use different settings. +You can also configure a tenant to use different settings. The next sample shows you how to customize the values. - + + +Custom tenant configuration + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. +You can edit the values by clicking on the pencil icon and then specifying a new value. + +:::warning[You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them.] + +::: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + coreConfig: { + email_verification_token_lifetime: 7200000, + password_reset_token_lifetime: 3600000, + postgresql_connection_uri: "postgresql://localhost:5432/db2", + }, + }); -### 3. View tenant details + if (resp.createdNew) { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ + CoreConfig: map[string]interface{}{ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, + }) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( + core_config={ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("new tenant created") + else: + print("existing tenant's config was modified.") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +tenant_id = "customer1" +result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( + core_config={ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("new tenant created") +else: + print("existing tenant's config was modified.") +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "coreConfig": { + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2" + } +}' +``` + + + + + + + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + + +### 3. View tenant details To view the configuration for a specific tenant you can use an SDK method or call the API directly. - + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function getTenant(tenantId: string) { + let resp = await Multitenancy.getTenant(tenantId); + + if (resp === undefined) { + // tenant does not exist + } else { + let coreConfig = resp.coreConfig; + + let firstFactors = resp.firstFactors; + + let configuredThirdPartyProviders = resp.thirdParty.providers; + } +} +``` + + +```go +import ( + "fmt" + + "github.com/supertokens/supertokens-golang/recipe/multitenancy" +) + +func main() { + tenantId := "customer1" + + tenant, err := multitenancy.GetTenant(tenantId) + + if err != nil { + // handle error + } + if tenant == nil { + // tenant does not exist + } else { + isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled; + isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; + isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; + + if (isEmailPasswordLoginEnabled) { + // Tenant support email password login + } + + if (isThirdPartyLoginEnabled) { + // Tenant support third party login + configuredThirdPartyProviders := tenant.ThirdParty.Providers; + fmt.Println(configuredThirdPartyProviders); + } + + if (isPasswordlessLoginEnabled) { + // Tenant support passwordless login + } + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import get_tenant + +async def some_func(): + tenant = await get_tenant("customer1") + + if tenant is None: + print("tenant does not exist") + else: + core_config = tenant.core_config + first_factors = tenant.first_factors + providers = tenant.third_party_providers + + print(core_config) + print(first_factors) + print(providers) +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import get_tenant + +tenant = get_tenant("customer1") + +if tenant is None: + print("tenant does not exist") +else: + core_config = tenant.core_config + first_factors = tenant.first_factors + providers = tenant.third_party_providers + + print(core_config) + print(first_factors) + print(providers) +``` + + + + +```bash +curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' +``` + + + + + +Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). + +If the input tenant does not exist, you get back a `200` status code with the following JSON: + + + + + +```json +{ "status": "TENANT_NOT_FOUND_ERROR" } +``` + + + + + +Otherwise you get a `200` status code with the following JSON output: + + + + + +```json check=false reason="The providers array is abbreviated in this example response." +{ + "status": "OK", + "thirdParty": { + "providers": [...] + }, + "coreConfig": { + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2" + }, + "tenantId": "customer1", + "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] +} +``` + + + +The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. ### 4. Set up the user interface To allow users to authenticate using one of your previously created tenants you need to update your frontend application. -You can do this in two ways: [through a common domain](/docs/authentication/enterprise/common-domain-login), [through subdomains](/docs/authentication/enterprise/subdomain-login). +You can do this in two ways: [through a common domain](/authentication/enterprise/common-domain-login), [through subdomains](/authentication/enterprise/subdomain-login). Explore the two guides for a full list of instructions on how to implement the flows. ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/enterprise/introduction.mdx b/docs/authentication/enterprise/introduction.mdx index 4763efe0fc..9023a0934c 100644 --- a/docs/authentication/enterprise/introduction.mdx +++ b/docs/authentication/enterprise/introduction.mdx @@ -1,21 +1,14 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true -description: >- - Explore how you can implement enterprise authentication flows with - SuperTokens. -page_type: overview -recipe: thirdparty -category: enterprise-login +description: Explore how you can implement enterprise authentication flows with SuperTokens. +sidebar: + order: 10 --- -# Enterprise login - - ## Overview + + **SuperTokens** covers enterprise authentication scenarios through the **multi-tenancy** feature. Tenants represent isolated user groups that can only access their specific data. Furthermore, each tenant can have different methods of logging in, configured by the tenant, or by you (the application developer). @@ -37,82 +30,44 @@ The application would then route the user to their specific tenant, which could | Multiple development environments | Create multiple environments for development and CI/CD testing purposes | | Flexible tenant discovery | Authenticate users through different subdomains or based on a custom tenant selection | -## Prerequisites - - -## Getting started +## Getting started The initial setup guide shows you how to create a tenant and configure authentication for it. -After that, you have to implement the tenant discovery flow. +After that, you have to implement the tenant discovery flow. Choose between the common domain login and subdomain login methods, based on your use case. Before you explore the guides, read through the **Important concepts** page first. -It explains how multi tenancy works in more detail. - - - - - Important concepts - - - Go through a reference guide that explains all the multi-tenancy related concepts that are specific to SuperTokens. - - - - - Initial setup - - - Create a new tenant and configure the login methods on it. - - - - - - Implement common domain login - - - Allow tenants to login using the same domain. - - - - - Implement subdomain login - - - Allow tenants to login using different subdomains. - - - - -## Customization +It explains how multi tenancy works in more detail. + + + +Go through a reference guide that explains all the multi-tenancy related concepts that are specific to SuperTokens. + + +Create a new tenant and configure the login methods on it. + + + +Allow tenants to login using the same domain. + + +Allow tenants to login using different subdomains. + + + +## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Manage tenants - - - Discover all the common actions that you can perform on tenants. - - - - - Manage apps - - - Discover all the common actions that you can perform on applications. - - - - - SAML - - - See how you can implement SAML login. - - - + + +Discover all the common actions that you can perform on tenants. + + +Discover all the common actions that you can perform on applications. + + +See how you can implement SAML login. + + diff --git a/docs/authentication/enterprise/legacy-saml.mdx b/docs/authentication/enterprise/legacy-saml.mdx index 14693a3c7d..67b377687b 100644 --- a/docs/authentication/enterprise/legacy-saml.mdx +++ b/docs/authentication/enterprise/legacy-saml.mdx @@ -1,25 +1,20 @@ --- title: Legacy SAML -hide_title: true -sidebar_position: 11 -description: >- - Learn how SAML facilitates secure information exchange between authentication - servers and client applications. -page_type: guide -recipe: thirdparty -category: enterprise-login +description: Learn how SAML facilitates secure information exchange between authentication servers and client applications. +sidebar: + order: 110 --- -# Legacy SAML - - ## Overview The following guide shows you how to configure SAML using the legacy setup with SuperTokens. -:::warning no-title -Since version `11.3` of the core service you can use SuperTokens as a SAML client. -We recommend that you use the latest version with [the simplified setup](/docs/authentication/enterprise/saml). +:::warning[Since version `11.3` of the core service you can use SuperTokens as a SAML client.] +We recommend that you use the latest version with [the simplified setup](/authentication/enterprise/saml). +::: + +:::danger[Archived, non-operational reference] +Do not use the commands on this page for a new or production deployment. This legacy flow has no currently validated, pinned Jackson release. The versioned contract for the Polis replacement has not been established for `/api/v1/saml/config`, SuperTokens-managed hosting, provider-name aliases, or the returned client credentials. The examples below are retained only to explain the historical integration and must not be treated as current Polis APIs. ::: ### BoxyHQ @@ -36,7 +31,7 @@ Self hosted SuperTokens only supports PostgreSQL as a data source. The HTTP service uses NodeJS, and is embeddable within your NodeJS backend. However, because SuperTokens supports multiple backends, the focus is on deploying `SAML` Jackson as a microservice. -Flowchart of integrating a `SAML` provider with SuperTokens using `SAML` Jackson ( BoxyHQ ) +Flowchart of integrating a `SAML` provider with SuperTokens using `SAML` Jackson ( BoxyHQ ) 1. The user clicks on the login button and redirects to `SAML` Jackson's microservice at `http://localhost:5225/api/oauth/authorize` 2. `SAML` Jackson redirects the user to the `SAML` provider's login page where the user needs to enter their credentials @@ -47,22 +42,16 @@ However, because SuperTokens supports multiple backends, the focus is on deployi 7. SuperTokens' backend `SDK` creates a new user in the core associated with the end user's email. New session tokens are also created 8. A SuperTokens' session establishes between your app's backend and frontend - logging in the user. -:::info Example App +:::info[Example App] An [example app on GitHub](https://github.com/supertokens/jackson-supertokens-express) with SuperTokens + `SAML` Jackson, for React and NodeJS express apps, is available. This uses [mocksaml.com](https://mocksaml.com/) as a `SAML` provider ::: ## Before you start These instructions assume that you already are familiar with **SuperTokens** and you have configured a demo application. -If you have skipped those steps page please go through the main [quickstart guide](/docs/quickstart/introduction). - -## Using the SuperTokens dashboard - -:::caution +If you have skipped those steps, follow the main [quickstart guide](/quickstart). -This is only available with Node and Python SDKs. - -::: +## Using the SuperTokens dashboard ### 1. Generate the XML metadata file from your SAML provider Your SAML provider allows you to download a `.xml` file that you can upload to SAML Jackson. During this process, you need to provide it: @@ -75,51 +64,33 @@ In the example app, [mocksaml.com](https://mocksaml.com/) serves as a free SAML ### 2. Start the SAML Jackson service -You can run SAML Jackson [with or without Docker](https://boxyhq.com/docs/jackson/deploy/service). -```bash -docker run \ - -p 5225:5225 \ - -e JACKSON_API_KEYS="secret" \ - -e DB_ENGINE="sql" \ - -e DB_TYPE="postgres" \ - -e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \ - -d boxyhq/jackson -``` - -This starts the SAML Jackson server on `http://localhost:5225`. - -:::important - -If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). - -::: +The former unpinned `boxyhq/jackson` deployment command has been removed because it does not identify a reproducible, validated release. Do not substitute `boxyhq/jackson:latest`. To recover this flow, first establish and document a known-compatible image digest, its required environment variables, and its versioned API contract. ### 3. Create a new tenant in SuperTokens (if not done already) -Create Tenant +Create Tenant ### 4. Configure the SAML provider for the tenant -Create Tenant +Create Tenant To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party configuration. Make sure to specify provider name with one of the following: -- Microsoft Entra ID
-- Microsoft AD FS
-- Okta
-- Auth0
-- Google
-- OneLogin
-- PingOne
-- JumpCloud
-- Rippling
-- SAML
- -Make sure to replace `http://localhost:5225` with the correct value for where you have hosted the BoxyHQ server. If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). - -:::success -You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows. +- Microsoft Entra ID
+- Microsoft AD FS
+- Okta
+- Auth0
+- Google
+- OneLogin
+- PingOne
+- JumpCloud
+- Rippling
+- SAML
+ +The historical dashboard flow assumed a separately hosted BoxyHQ server. SuperTokens-managed hosting for this legacy integration has not been validated against a current, versioned contract. + +:::success[You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows.] ::: ### 5. Adding multiple SAML connections to a single tenant @@ -132,7 +103,7 @@ For example, if a tenant has Active Directory and Okta login (both with SAML), y For Okta, you could use `okta` as a suffix to make the `Third Party Id` equal to `"boxy-saml-okta"`. You can also give them different names. Instead of "SAML Login" (that's shown above), you can use "Active Directory" and "Okta" to ensure that the button on the pre-built UI shows the right name. ---- +--- ## Using the BoxyHQ API @@ -176,27 +147,11 @@ PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PEVudGl0 ### 3. Start the SAML Jackson service -You can run SAML Jackson [with or without Docker](https://boxyhq.com/docs/jackson/deploy/service). -```bash -docker run \ - -p 5225:5225 \ - -e JACKSON_API_KEYS="secret" \ - -e DB_ENGINE="sql" \ - -e DB_TYPE="postgres" \ - -e DB_URL="postgres://postgres:postgres@postgres:5432/postgres" \ - -d boxyhq/jackson -``` +The historical deployment command is intentionally omitted. A compatible Jackson image version or digest and its deployment contract have not been established. -This starts the SAML Jackson server on `http://localhost:5225`. +### 4. Historical SAML Jackson configuration request -:::important - -If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). - -::: - -### 4. Upload the base64 XML string to SAML Jackson -Take the string generated in step (2) and run: +The following request records the old integration shape only. Do not send it to Polis: no versioned authoritative contract has confirmed that Polis supports this route, fields, authentication scheme, or response credentials. ```bash curl --location --request POST 'http://localhost:5225/api/v1/saml/config' \ @@ -226,29 +181,25 @@ The output of this command provides you the `client_id` and `client_secret` for ### 5. Create a new tenant in SuperTokens (if not done already) - - - - + + ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { - let resp = await Multiteancy.createOrUpdateTenant("customer1", { - firstFactors: ["thirdparty"] - }); - - if (resp.createdNew) { - // new tenant was created - } else { - // existing tenant's config was modified. - } + let resp = await Multiteancy.createOrUpdateTenant("customer1", { + firstFactors: ["thirdparty"], + }); + + if (resp.createdNew) { + // new tenant was created + } else { + // existing tenant's config was modified. + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -259,11 +210,9 @@ func main() { tenantId := "customer1" thirdPartyEnabled := true - // highlight-start resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ ThirdPartyEnabled: &thirdPartyEnabled, }) - // highlight-end if err != nil { // handle error @@ -275,20 +224,17 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate async def some_func(): result = await create_or_update_tenant( - "public", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) + "customer1", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) ) if result.status != "OK": @@ -298,18 +244,15 @@ async def some_func(): else: print("existing tenant's config was modified.") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate def some_func(): result = create_or_update_tenant( - "public", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) + "customer1", TenantConfigCreateOrUpdate(first_factors=["thirdparty"]) ) if result.status != "OK": @@ -319,51 +262,45 @@ def some_func(): else: print("existing tenant's config was modified.") ``` + + + + - - - - - - - - - - + + ### 6. Configure the SAML provider for the tenant - - - - + + ```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy" +import Multitenancy from "supertokens-node/recipe/multitenancy"; async function addThirdPartyConfigToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "boxy-saml", - name: "", - clients: [{ - clientId: "", - clientSecret: "", - additionalConfig: { - "boxyURL": "http://localhost:5225", - } - }] - }); - - if (resp.createdNew) { - // SAML Login added to customer1 - } else { - // Existing SAML Login config overwritten for customer1 - } + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "boxy-saml", + name: "", + clients: [ + { + clientId: "", + clientSecret: "", + additionalConfig: { + boxyURL: "http://localhost:5225", + }, + }, + ], + }); + + if (resp.createdNew) { + // SAML Login added to customer1 + } else { + // Existing SAML Login config overwritten for customer1 + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -373,7 +310,6 @@ import ( func main() { tenantId := "customer1" - // highlight-start resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "boxy-saml", Name: "", @@ -387,7 +323,6 @@ func main() { }, }, }, nil) - // highlight-end if err != nil { // handle error @@ -399,13 +334,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig @@ -432,11 +364,8 @@ async def some_func(): else: print("Existing SAML Login config overwritten for customer1") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig @@ -462,36 +391,31 @@ elif result.created_new: else: print("Existing SAML Login config overwritten for customer1") ``` + + + + - - - - - - - - - - + + To configure SAML login with SuperTokens, ensure that you use the correct provider name in the third-party configuration. Make sure to replace `` in the code snippet above with one of the following -- Microsoft Entra ID
-- Microsoft AD FS
-- Okta
-- Auth0
-- Google
-- OneLogin
-- PingOne
-- JumpCloud
-- Rippling
-- SAML
- -Make sure to replace `http://localhost:5225` with the correct value for where you have hosted the BoxyHQ server. If you are using the SuperTokens managed service, Boxy HQ hosts the server for you ([contact support](mailto:support@supertokens.com) to activate your instance). - -:::success -You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows. +- Microsoft Entra ID
+- Microsoft AD FS
+- Okta
+- Auth0
+- Google
+- OneLogin
+- PingOne
+- JumpCloud
+- Rippling
+- SAML
+ +The provider configuration above is historical. Current SuperTokens-managed hosting and the replacement service contract have not been validated. + +:::success[You have successfully configured a new tenant in SuperTokens. The next step is to wire up the frontend SDK to show the right login UI for this tenant. The specifics of this step depend on the UX that you want to provide to your users. The "Common UX flows" section documents two common UX flows.] ::: ### 7. Adding multiple SAML connections to a single tenant @@ -507,10 +431,10 @@ For example, if a tenant has Active Directory and Okta login (both with SAML), y ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/enterprise/manage-apps.mdx b/docs/authentication/enterprise/manage-apps.mdx index f98403112a..d8938cdcb4 100644 --- a/docs/authentication/enterprise/manage-apps.mdx +++ b/docs/authentication/enterprise/manage-apps.mdx @@ -1,18 +1,10 @@ --- title: Manage apps -hide_title: true -sidebar_position: 7 description: Create and manage multiple apps within a SuperTokens integration. -page_type: guide -recipe: thirdparty -category: enterprise-login +sidebar: + order: 90 --- - - -# Manage apps - - ## Run multiple apps using the same SuperTokens core @@ -26,15 +18,15 @@ You can use this feature to deploy one SuperTokens core across multiple independ ### 1. Create a new app in the core -:::caution +:::warning This is a paid feature, even if creating an additional `dev` `env` on the managed service, or if using the `dev` license keys in case of self-hosting. The pricing is $50 / month / additional app. Please reach out to [support@SuperTokens.com](mailto:support@SuperTokens.com) if you have any questions, or if you want to create multiple `environments` and want a bulk discount. ::: To create a new app in the SuperTokens core, you can use the following cURL command: ```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/multitenancy/app/v2' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/recipe/multitenancy/app/v2' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", @@ -57,8 +49,8 @@ The built-in Factor IDs that you can use for `firstFactors` are: - With SMS magic link: `link-phone` ```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/multitenancy/app' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/recipe/multitenancy/app' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1", @@ -74,72 +66,62 @@ curl --location --request PUT '^{coreInfo.uri}/recipe/multitenancy/app' \ - You can set core configurations for this app (see the configuration.yaml / docker environment variable options for your core). The core configurations for a new app inherit from the configurations provided in the configuration.yaml / docker environment (or the **Configuration** page for the managed deployment). - By default, all the login methods enable for a new app (specifically, the `public` tenant of the new app), but you can pass in `false` to any of the login methods specified above to disable them. -:::important -Even if a login method enables for a tenant, you still require to initialize the right recipe on the backend for sign up / in to be possible with that login method. For example, if for a tenant, you have enabled the passwordless login method, but don't use the passwordless (or a combination recipe that has passwordless) on the backend, then end users cannot sign up / in using the passwordless APIs because those APIs are not exposed via the backend SDK's middleware. +:::note[Even if a login method enables for a tenant, you still require to initialize the right recipe on the backend for sign up / in to be possible with that login method. For example, if for a tenant, you have enabled the passwordless login method, but don't use the passwordless (or a combination recipe that has passwordless) on the backend, then end users cannot sign up / in using the passwordless APIs because those APIs are not exposed via the backend SDK's middleware.] ::: ### 2. Configure the `appId` during backend SDK init Whilst one core can have multiple apps, you must use a dedicated backend (integrated with the backend SDK) per app. For example, if you have two apps, and both use a NodeJS backend, then you need to configure one app's backend to have `appId` as `app1` (as an example). The other app's backend should have `appId` as `app2`. You can specify an `appId` on the backend SDK SuperTokens.init by appending the `appId` to the `connectionUri` as shown below: - - - + + ```tsx import supertokens from "supertokens-node"; supertokens.init({ - supertokens: { - // highlight-next-line - connectionURI: "http://localhost:3567/appid-app1", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [] + supertokens: { + connectionURI: "http://localhost:3567/appid-app1", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], }); ``` - - - - + + ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ - // highlight-next-line ConnectionURI: "http://localhost:3567/appid-app1", }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( - # highlight-next-line connection_uri='http://localhost:3567/appid-app1', ), - framework='...', # type: ignore + framework='...', recipe_list=[ #... ] ) ``` - - - + + - In the above code snippet, the backend SDK receives information that the `appId` to use for this app is `app1`. You can pick your own app ID, but whatever it is, you need to add it as shown above. - It is important to prefix the app ID with `appid-` as that enables the SuperTokens core to reliably detect the app that the query is for. @@ -151,14 +133,14 @@ init( You can only perform this via a cURL command. No helper function exists for this in the backend SDKs since the backend SDKs are per app anyway. ```bash -curl --location --request GET '^{coreInfo.uri}/recipe/multitenancy/app/list/v2' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request GET '/recipe/multitenancy/app/list/v2' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' ``` You get the following JSON output: -```json +```json check=false reason="The nested tenant fields are abbreviated for readability." { "status": "OK", "apps": [{ @@ -176,14 +158,14 @@ You get the following JSON output: ``` ```bash -curl --location --request GET '^{coreInfo.uri}/recipe/multitenancy/app/list' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request GET '/recipe/multitenancy/app/list' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' ``` You get the following JSON output: -```json +```json check=false reason="The nested tenant fields are abbreviated for readability." { "status": "OK", "apps": [{ @@ -213,8 +195,7 @@ You get the following JSON output: The following snippet shows you how to delete an app from a SuperTokens Core instance. This operation is irreversible and deletes all user data associated with the app. -:::important -Before you delete an app, ensure that you satisfy the following requirements: +:::note[Before you delete an app, ensure that you satisfy the following requirements:] - The request must originate from the public app and tenant - The app must not have any tenants other than the public tenant. You need to delete other tenants first. @@ -223,8 +204,8 @@ After deleting an app, make sure to update any backend services configured to us ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/multitenancy/app/remove' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/multitenancy/app/remove' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "appId": "app1" @@ -235,6 +216,5 @@ curl --location --request POST '^{coreInfo.uri}/recipe/multitenancy/app/remove' - All user data, configuration, and tenant information associated with this app are permanently deleted. - The API key used must have the necessary permissions to delete apps. -:::danger -This operation cannot be undone. Make sure you have backed up any important data before proceeding. +:::danger[This operation cannot be undone. Make sure you have backed up any important data before proceeding.] ::: diff --git a/docs/authentication/enterprise/manage-tenants.mdx b/docs/authentication/enterprise/manage-tenants.mdx index c6334927d5..5172f10596 100644 --- a/docs/authentication/enterprise/manage-tenants.mdx +++ b/docs/authentication/enterprise/manage-tenants.mdx @@ -1,24 +1,179 @@ --- title: Tenant actions -hide_title: true -sidebar_position: 6 description: Discover different APIs that can help you work with tenants. -page_type: guide -recipe: thirdparty -category: enterprise-login +sidebar: + order: 70 --- -import CreateTenantBlock from "./_blocks/create-tenant.mdx"; -import UpdateTenantConfigBlock from "./_blocks/update-tenant-config.mdx"; -import GetTenantConfigBlock from "./_blocks/get-tenant.mdx"; +## Create a new tenant + + +Create Tenant -# Manage tenants +Create a new tenant by clicking on the **Add Tenant** button and specify the tenant ID. +All Login Methods Enabled -## Create a new tenant +Once you create the tenant, turn on the Login Methods as required for the tenant. In the above example, you turn on all the Login Methods. + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + firstFactors: ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"], + }); + + if (resp.createdNew) { + // Tenant created successfully + } else { + // Existing tenant's config was modified. + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" +) + +func main() { + tenantId := "customer1" + emailPasswordEnabled := true + thirdPartyEnabled := true + passwordlessEnabled := true + + resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ + EmailPasswordEnabled: &emailPasswordEnabled, + ThirdPartyEnabled: &thirdPartyEnabled, + PasswordlessEnabled: &passwordlessEnabled, + }) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +async def some_func(): + response = await create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( + first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] + )) + + if response.status != "OK": + print("Handle error") + elif response.created_new: + print("New tenant was created") + else: + print("Existing tenant's config was updated") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +def some_func(): + response = create_or_update_tenant("customer1", TenantConfigCreateOrUpdate( + first_factors=["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-phone", "link-email"] + )) - + if response.status != "OK": + print("Handle error") + elif response.created_new: + print("New tenant was created") + else: + print("Existing tenant's config was updated") +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] +}' +``` + + + + + + + + +The snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by not including them in the `firstFactors` input. +If `firstFactors` is not specified, by default, the system does not enable any of the login methods. + +If you set `firstFactors` to `null` the SDK uses any of the login methods. + +The built-in Factor IDs available for `firstFactors` include: + +| Authentication Type | Factor ID | +|-------------------|-----------| +| Email password auth | `emailpassword` | +| Social login / enterprise SSO auth | `thirdparty` | +| Passwordless - Email OTP | `otp-email` | +| Passwordless - SMS OTP | `otp-phone` | +| Passwordless - Email magic link | `link-email` | +| Passwordless - SMS magic link | `link-phone` | + + +The code snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by setting the corresponding field to `false`. + + +The code snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by setting the corresponding field to `false`. + + +The request includes the `appId` for which you need to create a new tenant. +If you are using the default (`"public"`) app, you can omit the `/appid-` part of the URL. + +The snippet creates a new tenant with the id `"customer1"`. +It enables the email password, third party and passwordless login methods for this tenant. +You can also disable any of these by not including them in the `firstFactors` input. +If `firstFactors` is not specified, by default, the system does not enable any of the login methods. + +The built-in Factor IDs available for `firstFactors` include: + +| Authentication Type | Factor ID | +|-------------------|-----------| +| Email password auth | `emailpassword` | +| Social login / enterprise SSO auth | `thirdparty` | +| Passwordless - Email OTP | `otp-email` | +| Passwordless - SMS OTP | `otp-phone` | +| Passwordless - Email magic link | `link-email` | +| Passwordless - SMS magic link | `link-phone` | + + --- @@ -26,7 +181,182 @@ import GetTenantConfigBlock from "./_blocks/get-tenant.mdx"; You can also configure a tenant to have different configurations per the core's `config.yaml` or docker environment variables. Below is how you can specify the configuration, when creating or modifying a tenant: - + + +Custom tenant configuration + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. +You can edit the values by clicking on the pencil icon and then specifying a new value. + +:::warning[You cannot edit database connection settings directly from the Dashboard, and you may need to use the SDK or cURL to update them.] + +::: + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function createNewTenant() { + let resp = await Multitenancy.createOrUpdateTenant("customer1", { + coreConfig: { + email_verification_token_lifetime: 7200000, + password_reset_token_lifetime: 3600000, + postgresql_connection_uri: "postgresql://localhost:5432/db2", + }, + }); + + if (resp.createdNew) { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/multitenancy/multitenancymodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateTenant(tenantId, multitenancymodels.TenantConfig{ + CoreConfig: map[string]interface{}{ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, + }) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // new tenant was created + } else { + // existing tenant's config was modified. + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( + core_config={ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("new tenant created") + else: + print("existing tenant's config was modified.") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_tenant +from supertokens_python.recipe.multitenancy.interfaces import TenantConfigCreateOrUpdate + +tenant_id = "customer1" +result = create_or_update_tenant(tenant_id, TenantConfigCreateOrUpdate( + core_config={ + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2", + }, +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("new tenant created") +else: + print("existing tenant's config was modified.") +``` + + + + +```bash +curl --location --request PUT 'http://localhost:3567/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "tenantId": "customer1", + "coreConfig": { + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2" + } +}' +``` + + + + + + + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + +In the above example, the system assigns different values for certain configurations for `customer1` tenant. +All other configurations inherit from the base configuration. + +Notice the `postgresql_connection_uri`. +This allows you to achieve **data isolation on a tenant level**. +This configuration is not required. +If not provided, the database stores the tenant's information as specified in the core's configuration. +It is still a different user pool though. + + --- @@ -34,56 +364,193 @@ You can also configure a tenant to have different configurations per the core's Once you have set the configs for a specific tenant, you can fetch the tenant info as shown below: - + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; ---- +async function getTenant(tenantId: string) { + let resp = await Multitenancy.getTenant(tenantId); -## List all the tenants of an app + if (resp === undefined) { + // tenant does not exist + } else { + let coreConfig = resp.coreConfig; - + let firstFactors = resp.firstFactors; - + let configuredThirdPartyProviders = resp.thirdParty.providers; + } +} +``` + + +```go +import ( + "fmt" -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; + "github.com/supertokens/supertokens-golang/recipe/multitenancy" +) -async function listAllTenants() { +func main() { + tenantId := "customer1" + + tenant, err := multitenancy.GetTenant(tenantId) + + if err != nil { + // handle error + } + if tenant == nil { + // tenant does not exist + } else { + isEmailPasswordLoginEnabled := tenant.EmailPassword.Enabled; + isThirdPartyLoginEnabled := tenant.ThirdParty.Enabled; + isPasswordlessLoginEnabled := tenant.Passwordless.Enabled; + + if (isEmailPasswordLoginEnabled) { + // Tenant support email password login + } + + if (isThirdPartyLoginEnabled) { + // Tenant support third party login + configuredThirdPartyProviders := tenant.ThirdParty.Providers; + fmt.Println(configuredThirdPartyProviders); + } + + if (isPasswordlessLoginEnabled) { + // Tenant support passwordless login + } + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import get_tenant + +async def some_func(): + tenant = await get_tenant("customer1") + + if tenant is None: + print("tenant does not exist") + else: + core_config = tenant.core_config + first_factors = tenant.first_factors + providers = tenant.third_party_providers + + print(core_config) + print(first_factors) + print(providers) +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import get_tenant + +tenant = get_tenant("customer1") + +if tenant is None: + print("tenant does not exist") +else: + core_config = tenant.core_config + first_factors = tenant.first_factors + providers = tenant.third_party_providers - // highlight-start - let resp = await Multitenancy.listAllTenants(); - let tenants = resp.tenants; - // highlight-end + print(core_config) + print(first_factors) + print(providers) +``` + + + + +```bash +curl --location --request GET 'http://localhost:3567/customer1/recipe/multitenancy/tenant/v2' \ +--header 'api-key: YOUR_API_KEY' \ +--header 'Content-Type: application/json' +``` + + - tenants.forEach(tenant => { - let coreConfig = tenant.coreConfig; + + +Notice that you add `customer1` to the path of the request. This tells the core that the tenant you want to get the information about is `customer1` (the one created before in this page). - let firstFactors = tenant.firstFactors; +If the input tenant does not exist, you get back a `200` status code with the following JSON: + + - let configuredThirdPartyProviders = tenant.thirdParty.providers; - }); + + +```json +{ "status": "TENANT_NOT_FOUND_ERROR" } +``` + + + + + +Otherwise you get a `200` status code with the following JSON output: + + + + + +```json check=false reason="The tenant response fields are abbreviated for readability." +{ + "status": "OK", + "thirdParty": { + "providers": [...] + }, + "coreConfig": { + "email_verification_token_lifetime": 7200000, + "password_reset_token_lifetime": 3600000, + "postgresql_connection_uri": "postgresql://localhost:5432/db2" + }, + "tenantId": "customer1", + "firstFactors": ["emailpassword", "thirdparty", "otp-email", "otp-phone", "link-email", "link-phone"] } ``` + + -The value of `firstFactors` can be as follows: +The returned `coreConfig` is the same as what you set when creating / updating the tenant. The rest of the core configurations for this tenant inherit from the app's (or the `public` tenant) configuration. The `public` tenant, for the `public` app inherits its configurations from the `config.yaml` / docker environment variables values. -- `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. -- `[]` (empty array): The tenant does not enable any login methods. -- a non-empty array: The tenant enables only the login methods in the array. +--- - - +## List all the tenants of an app + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function listAllTenants() { + let resp = await Multitenancy.listAllTenants(); + let tenants = resp.tenants; + + tenants.forEach((tenant) => { + let coreConfig = tenant.coreConfig; + + let firstFactors = tenant.firstFactors; + + let configuredThirdPartyProviders = tenant.thirdParty.providers; + }); +} +``` + + ```go import ( "fmt" - + "github.com/supertokens/supertokens-golang/recipe/multitenancy" ) func main() { - // highlight-start resp, err := multitenancy.ListAllTenants() - // highlight-end if err != nil { // handle error @@ -115,13 +582,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import list_all_tenants @@ -131,7 +595,7 @@ async def some_func(): if response.status != "OK": print("Handle error") return - + for tenant in response.tenants: core_configuration = tenant.core_config @@ -143,11 +607,8 @@ async def some_func(): print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import list_all_tenants @@ -157,7 +618,7 @@ def some_func(): if response.status != "OK": print("Handle error") return - + for tenant in response.tenants: core_config = tenant.core_config @@ -169,23 +630,34 @@ def some_func(): print(f"First factors: {first_factors}") print(f"Configured third party providers: {configured_third_party_providers}") ``` - - - - - - - - + + + + ```bash -curl --location --request GET '^{coreInfo.uri}/recipe/multitenancy/tenant/list/v2' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request GET '/recipe/multitenancy/tenant/list/v2' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' ``` + + + + + +The value of `firstFactors` can be as follows: +- `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. +- `[]` (empty array): The tenant does not enable any login methods. +- a non-empty array: The tenant enables only the login methods in the array. + + You get the following JSON output: + + -```json + + +```json check=false reason="The tenant response fields are abbreviated for readability." { "status": "OK", "tenants": [{ @@ -198,26 +670,28 @@ You get the following JSON output: }] } ``` + + + + The value of `firstFactors` can be as follows: - `undefined`: The core enables all login methods, and any auth recipe initialized in the backend SDK works. - `[]` (empty array): The tenant does not enable any login methods. - a non-empty array: The tenant enables only the login methods in the array. - - - - + + --- ## Add a custom third-party provider to a tenant -If you can't find a provider in [the built-in list](/docs/authentication/social/built-in-providers-config), you can add your own custom implementation. +If you can't find a provider in [the built-in list](/authentication/social/built-in-providers-config), you can add your own custom implementation. This page shows you how to do that on a per tenant basis. -:::info Note +:::info[Note] If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: @@ -226,52 +700,67 @@ Once you have created a tenant, you want to call the API / function to create a ### Using OAuth endpoints - + + +Click on **Add new provider** in the Social/Enterprise Providers section - +Social/Enterprise providers +Select **Add Custom Provider** option + +New Provider + +Fill in the details as shown below and click on **Save** + +OAuth2 provider + + + + + ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { - let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "custom", - name: "Custom Provider", - clients: [{ - clientId: "...", - clientSecret: "...", - scope: ["email", "profile"] - }], - authorizationEndpoint: "https://example.com/oauth/authorize", - authorizationEndpointQueryParams: { // optional - "someKey1": "value1", - "someKey2": null, - }, - tokenEndpoint: "https://example.com/oauth/token", - tokenEndpointBodyParams: { - "someKey1": "value1", - }, - userInfoEndpoint: "https://example.com/oauth/userinfo", - userInfoMap: { - fromUserInfoAPI: { - userId: "id", - email: "email", - emailVerified: "email_verified", - } - } - }); + let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "custom", + name: "Custom Provider", + clients: [ + { + clientId: "...", + clientSecret: "...", + scope: ["email", "profile"], + }, + ], + authorizationEndpoint: "https://example.com/oauth/authorize", + authorizationEndpointQueryParams: { + // optional + someKey1: "value1", + someKey2: null, + }, + tokenEndpoint: "https://example.com/oauth/token", + tokenEndpointBodyParams: { + someKey1: "value1", + }, + userInfoEndpoint: "https://example.com/oauth/userinfo", + userInfoMap: { + fromUserInfoAPI: { + userId: "id", + email: "email", + emailVerified: "email_verified", + }, + }, + }); - if (resp.createdNew) { - // custom provider added to tenant - } else { - // existing custom provider config overwritten for tenant - } + if (resp.createdNew) { + // custom provider added to tenant + } else { + // existing custom provider config overwritten for tenant + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -281,7 +770,6 @@ import ( func main() { tenantId := "..." - // highlight-start resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom Provider", @@ -310,7 +798,6 @@ func main() { }, }, }, nil) - // highlight-end if err != nil { // handle error @@ -322,13 +809,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields @@ -372,11 +856,8 @@ async def some_func(): else: print("Existing custom provider config overwritten for tenant") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields @@ -419,17 +900,13 @@ elif result.created_new: else: print("Existing custom provider config overwritten for tenant") ``` - - - - - - - - + + + + ```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { @@ -460,28 +937,13 @@ curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/c } }' ``` + + - - - - -Click on **Add new provider** in the Social/Enterprise Providers section - -Social/Enterprise providers + + -Select **Add Custom Provider** option - -New Provider - -Fill in the details as shown below and click on **Save** - -OAuth2 provider - - - - - -You can see all the options in the [CDI documentation](https://supertokens.com/docs/cdi). +You can see all the options in the [CDI documentation](https://supertokens.com/docs/references/cdi). | Field | Description | Example | |-------|-------------|---------| @@ -502,48 +964,62 @@ You can see all the options in the [CDI documentation](https://supertokens.com/d If the provider is Open ID Connect (OIDC) compatible, you can provide a URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. - - + + +Click on **Add new provider** in the Social/Enterprise Providers section + +Social/Enterprise providers + +Select **Add Custom Provider** option + +New Provider + +Fill in the details as shown below and click on **Save** +OAuth2 provider + + + + + ```tsx import Multiteancy from "supertokens-node/recipe/multitenancy"; async function createTenant() { - let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "custom", - name: "Custom Provider", - clients: [{ - clientId: "...", - clientSecret: "...", - scope: ["email", "profile"] - }], - // highlight-start - oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", - // highlight-end - authorizationEndpointQueryParams: { // optional - "someKey1": "value1", - "someKey2": null, - }, - userInfoMap: { - fromIdTokenPayload: { - userId: "id", - email: "email", - emailVerified: "email_verified", - } - } - }); + let resp = await Multiteancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "custom", + name: "Custom Provider", + clients: [ + { + clientId: "...", + clientSecret: "...", + scope: ["email", "profile"], + }, + ], + oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", + authorizationEndpointQueryParams: { + // optional + someKey1: "value1", + someKey2: null, + }, + userInfoMap: { + fromIdTokenPayload: { + userId: "id", + email: "email", + emailVerified: "email_verified", + }, + }, + }); - if (resp.createdNew) { - // custom provider added to tenant - } else { - // existing custom provider config overwritten for tenant - } + if (resp.createdNew) { + // custom provider added to tenant + } else { + // existing custom provider config overwritten for tenant + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -563,9 +1039,7 @@ func main() { Scope: []string{"profile", "email"}, }, }, - // highlight-start OIDCDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", - // highlight-end AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, @@ -589,10 +1063,8 @@ func main() { } } ``` - - - - + + ```python from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig, UserInfoMap, UserFields @@ -609,9 +1081,7 @@ async def some_func(): scope=["email", "profile"], ), ], - # highlight-start oidc_discovery_endpoint="https://example.com/.well-known/openid-configuration", - # highlight-end authorization_endpoint_query_params={ "someKey1": "value1", "someKey2": None, @@ -633,14 +1103,11 @@ async def some_func(): else: print("Existing custom provider config overwritten for tenant") ``` - - - - - + + ```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { @@ -666,28 +1133,13 @@ curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/c } }' ``` + + - - - - -Click on **Add new provider** in the Social/Enterprise Providers section - -Social/Enterprise providers - -Select **Add Custom Provider** option - -New Provider - -Fill in the details as shown below and click on **Save** + + -OAuth2 provider - - - - - -You can see all the options in the [CDI documentation](https://supertokens.com/docs/cdi). +You can see all the options in the [CDI documentation](https://supertokens.com/docs/references/cdi). | Field | Description | |-------|-------------| @@ -707,39 +1159,35 @@ For example, if a user signs up with email password login in the `public` tenant To associate a user with a tenant, you can call the following API: - - - - + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import {RecipeUserId} from "supertokens-node"; +import { RecipeUserId } from "supertokens-node"; async function addUserToTenant(recipeUserId: RecipeUserId, tenantId: string) { - let resp = await Multitenancy.associateUserToTenant(tenantId, recipeUserId); - - if (resp.status === "OK") { - // User is now associated with tenant - } else if (resp.status === "UNKNOWN_USER_ID_ERROR") { - // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. - } else if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { - // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. - } else if (resp.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { - // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. - } else if (resp.status === "ASSOCIATION_NOT_ALLOWED_ERROR") { - // This can happen if using account linking along with multi tenancy. One example of when this - // happens if if the target tenant has a primary user with the same email / phone numbers - // as the current user. - } else { - // status is THIRD_PARTY_USER_ALREADY_EXISTS_ERROR - // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. - } + let resp = await Multitenancy.associateUserToTenant(tenantId, recipeUserId); + + if (resp.status === "OK") { + // User is now associated with tenant + } else if (resp.status === "UNKNOWN_USER_ID_ERROR") { + // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. + } else if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. + } else if (resp.status === "PHONE_NUMBER_ALREADY_EXISTS_ERROR") { + // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. + } else if (resp.status === "ASSOCIATION_NOT_ALLOWED_ERROR") { + // This can happen if using account linking along with multi tenancy. One example of when this + // happens if if the target tenant has a primary user with the same email / phone numbers + // as the current user. + } else { + // status is THIRD_PARTY_USER_ALREADY_EXISTS_ERROR + // This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant. + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -759,7 +1207,7 @@ func main() { } else if resp.UnknownUserIdError != nil { // The provided user ID was not one that signed up using one of SuperTokens' auth recipes. } else if resp.EmailAlreadyExistsError != nil { - // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. + // This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. } else if resp.PhoneNumberAlreadyExistsError != nil { // This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. } else { @@ -768,13 +1216,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import associate_user_to_tenant from supertokens_python.recipe.multitenancy.interfaces import AssociateUserToTenantUnknownUserIdError, AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantNotAllowedError, AssociateUserToTenantOkResult @@ -800,11 +1245,8 @@ async def some_func(): print("status is ThirdPartyUserAlreadyExistsError") print("This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant.") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import associate_user_to_tenant from supertokens_python.recipe.multitenancy.interfaces import AssociateUserToTenantUnknownUserIdError, AssociateUserToTenantEmailAlreadyExistsError, AssociateUserToTenantPhoneNumberAlreadyExistsError, AssociateUserToTenantNotAllowedError, AssociateUserToTenantOkResult @@ -829,33 +1271,32 @@ else: print("status is ThirdPartyUserAlreadyExistsError") print("This means that the input user had already previously signed in with the same third party provider (e.g. Google) for the new tenant.") ``` - - - - - - - - + + + + ```bash -curl --location --request POST '^{coreInfo.uri}//recipe/multitenancy/tenant/user \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '//recipe/multitenancy/tenant/user \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "recipeUserId": "..." }' ``` + + + + In the above code, `recipeUserId` is associated with the tenant with ID `TENANT_ID`. The output of the above API has the following `status` response: + - `"OK"`: User association with tenant was successful - `"UNKNOWN_USER_ID_ERROR"`: The provided user ID was not one that signed up using one of SuperTokens' auth recipes. - `"EMAIL_ALREADY_EXISTS_ERROR"`: This means that the input user is one of passwordless or email password logins, and the new tenant already has a user with the same email for that login method. - `"PHONE_NUMBER_ALREADY_EXISTS_ERROR"`: This means that the input user is a passwordless user and the new tenant already has a user with the same phone number, for passwordless login. - `"THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"`: This means that the input user had already previously signed in with the same third-party provider (for example, Google) for the new tenant. - - - - + + --- @@ -863,28 +1304,24 @@ In the above code, `recipeUserId` is associated with the tenant with ID `TENANT_ You can even remove a user's access from a tenant using the API call shown below. In fact, you can remove a user from all tenants that they have access to, and the user and their metadata remain in the system. However, they cannot log in to any tenant. To remove a user from a tenant, call the following API: - - - - + + ```tsx import Multitenancy from "supertokens-node/recipe/multitenancy"; -import {RecipeUserId} from "supertokens-node"; +import { RecipeUserId } from "supertokens-node"; async function removeUserFromTeannt(recipeUserId: RecipeUserId, tenantId: string) { - let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); + let resp = await Multitenancy.disassociateUserFromTenant(tenantId, recipeUserId); - if (resp.wasAssociated) { - // User was removed from tenant - } else { - // User was never a part of the tenant anyway - } + if (resp.wasAssociated) { + // User was removed from tenant + } else { + // User was never a part of the tenant anyway + } } ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -906,13 +1343,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.multitenancy.asyncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId @@ -925,11 +1359,8 @@ async def some_func(): else: print("User was never a part of the tenant anyway") ``` - - - - - + + ```python from supertokens_python.recipe.multitenancy.syncio import disassociate_user_from_tenant from supertokens_python.types import RecipeUserId @@ -942,39 +1373,31 @@ def some_func(): else: print("User was never a part of the tenant anyway") ``` - - - - - - - - + + + + ```bash -curl --location --request POST '^{coreInfo.uri}//recipe/multitenancy/tenant/user/remove \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '//recipe/multitenancy/tenant/user/remove \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "recipeUserId": "..." }' ``` + + - - - - -:::important -- Users can only share access across tenants and not across apps. +:::note[- Users can only share access across tenants and not across apps.] - If your app has two tenants, that are in different database locations, then you cannot share users between them. ::: ## See also - - - - - - - + + + + + + diff --git a/docs/authentication/enterprise/meta.ts b/docs/authentication/enterprise/meta.ts new file mode 100644 index 0000000000..4f9e82e803 --- /dev/null +++ b/docs/authentication/enterprise/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Enterprise Login", + icon: "building-2", + order: 50, +}); diff --git a/docs/authentication/enterprise/saml.mdx b/docs/authentication/enterprise/saml.mdx index 5750f01c68..89b003ce3e 100644 --- a/docs/authentication/enterprise/saml.mdx +++ b/docs/authentication/enterprise/saml.mdx @@ -1,30 +1,24 @@ --- title: SAML -hide_title: true -sidebar_position: 10 -toc_max_heading_level: 4 description: Add SAML authentication to your application -page_type: tutorial -recipe: thirdparty -category: enterprise-login +sidebar: + order: 100 --- -# SAML Setup - ## Overview The following guide shows you how to configure SAML with your SuperTokens integration. `SAML`, or Security Assertion Markup Language, is an open protocol that exchanges information between the authentication server and the client application. -### How it works? +### How does it work? -Your `SAML` identity provider has a metadata file (`.xml`) that you or your end users need to upload to the `SAML` client. +Your SAML identity provider (IdP) has a metadata file (`.xml`) that you or your end users need to upload to the SAML service provider (SP). The `.xml` metadata file contains (amongst other things): -- A unique entity ID that you must keep private. The `SAML` provider uses this to identify your application. +- A unique entity ID that identifies the IdP. It is an identifier, not a secret, and may be shared as part of SAML metadata. - A public certificate that verifies the signature attached to the incoming `SAML` response. This ensures the response is coming from the expected Identity Provider. -- Information about where to redirect the end user to when they click on the login button in your application. +- Information about where to redirect the end user to when they click on the login button in your application. This URL is to a website controlled by the `SAML` provider and asks the end user for their credentials. @@ -32,23 +26,18 @@ This URL is to a website controlled by the `SAML` provider and asks the end user -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page please follow the tutorial and return here once you're done. - - -:::warning no-title -You need to use a SuperTokens core that is at least on version `11.3`. -The feature is only available with the `Node.js` SDK. -Support for `Python` and `Golang` is in active development. +:::warning[Use a current, patched SuperTokens Core] +SAML requires Core `12.0` or later. The feature is available in the Node.js and Python SDKs while Go is not currently supported. ::: + ## Steps -### 1. Get the SAML metadata from your Identity Provider +### 1. Get the SAML metadata from your identity provider -Before configuring SuperTokens, you need to obtain the SAML metadata XML from your Identity Provider (IDP). -This is typically available in your IDP's admin console as a downloadable XML file or a metadata URL. +Before configuring SuperTokens, you need to obtain the SAML metadata XML from your identity provider (IdP). +This is typically available in your IdP's admin console as a downloadable XML file or a metadata URL. Common locations for metadata: - **Azure AD**: Enterprise Applications > Your App > Single sign-on > Federation Metadata XML @@ -57,38 +46,66 @@ Common locations for metadata: ### 2. Initialize the SAML recipe in the backend SDK + + ```typescript -import { SuperTokens } from "supertokens-node"; +import SuperTokens from "supertokens-node"; import Saml from "supertokens-node/recipe/saml"; SuperTokens.init({ - supertokens: { - connectionURI: "", - apiKey: "", - }, - appInfo: { - appName: "App name", - apiDomain: "", - websiteDomain: "", - }, - recipeList: [ - // other recipes - Saml.init(), - ] + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + appName: "App name", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [ + // other recipes + Saml.init(), + ], }); - ``` + + +```python +from supertokens_python import InputAppInfo, SupertokensConfig, init +from supertokens_python.recipe import saml + + +init( + supertokens_config=SupertokensConfig( + connection_uri="", + api_key="", + ), + app_info=InputAppInfo( + app_name="App name", + api_domain="", + website_domain="", + ), + framework="fastapi", + recipe_list=[ + # Other recipes + saml.init(), + ], +) +``` + + ### 3. Create a new SAML client Use the metadata XML obtained in step 1 to create a SAML client. The `redirectURIs` should point to your application's callback URL where users will be redirected after authentication. -:::info no-title -This step assumes that you previously have created a SuperTokens tenant. -If you have not, please follow the [initial setup guide](/docs/authentication/enterprise/initial-setup). +:::info[This step assumes that you previously have created a SuperTokens tenant.] +If you have not, please follow the [initial setup guide](/authentication/enterprise/initial-setup). ::: + + ```typescript import Saml from "supertokens-node/recipe/saml"; @@ -104,11 +121,36 @@ async function createSamlClient() { enableRequestSigning: true, }); - // Save the clientId for use in the ThirdParty provider configuration - console.log("Client ID:", result.clientId); + if (result.status === "OK") { + // Save the clientId for use in the ThirdParty provider configuration + console.log("Client ID:", result.clientId); + } } - ``` + + +```python +from supertokens_python.recipe.saml.asyncio import create_or_update_client + + +async def create_saml_client(): + result = await create_or_update_client( + tenant_id="", + client_id="", + client_secret="", + redirect_uris=["https://your-app.com/auth/callback"], + default_redirect_uri="https://your-app.com/auth/callback", + metadata_xml="", + allow_idp_initiated_login=True, + enable_request_signing=True, + ) + + if result.status == "OK": + # Save the client ID for use in the ThirdParty provider configuration + print("Client ID:", result.client.client_id) +``` + + | Name | Type | Description | Required | @@ -124,67 +166,130 @@ async function createSamlClient() { | `userContext` | `Record` | An optional object containing additional context or metadata for the operation. | No | -### 4. Configure your Identity Provider +### 4. Configure your identity provider -After creating the SAML client, you need to configure your Identity Provider with your application's Service Provider (SP) details. +After creating the SAML client, you need to configure your identity provider with your application's service provider (SP) details. -On the IDP side, configure the following properties: -- **Entity ID**: Should match the `saml_sp_entity_id` value used in your [tenant configuration](/docs/authentication/enterprise/manage-tenants#update-a-tenant). The default value is `https://saml.supertokens.com`. -- **ACS URL** (Assertion Consumer Service URL): `/auth/saml/callback` +On the IdP side, configure the following properties: +- **Entity ID**: Should match the `saml_sp_entity_id` value used in your [tenant configuration](/authentication/enterprise/manage-tenants#update-a-tenant). The default value is `https://saml.supertokens.com`. +- **ACS URL** (Assertion Consumer Service URL): `/auth/`<TENANT_ID>`/saml/callback` ### 5. Add the ThirdParty provider Update your SuperTokens initialization to include the ThirdParty recipe with your SAML provider. The `thirdPartyId` must start with `saml-` followed by your custom identifier. + + ```typescript -import { SuperTokens } from "supertokens-node"; +import SuperTokens from "supertokens-node"; import Saml from "supertokens-node/recipe/saml"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - supertokens: { - connectionURI: "", - apiKey: "", - }, - appInfo: { - appName: "App name", - apiDomain: "", - websiteDomain: "", - }, - recipeList: [ - Saml.init(), - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - // Name that will be shown on the login page - name: "Azure SAML", - // Must start with "saml-" - thirdPartyId: "saml-azure", - clients: [ - { - // The clientId from step 3 - clientId: "", - }, - ], - }, - } - ], + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + appName: "App name", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [ + Saml.init(), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + // Name that will be shown on the login page + name: "Azure SAML", + // Must start with "saml-" + thirdPartyId: "saml-azure", + clients: [ + { + // The clientId from step 3 + clientId: "", + }, + ], }, - }), - ] + }, + ], + }, + }), + ], }); - ``` + + +```python +from supertokens_python import InputAppInfo, SupertokensConfig, init +from supertokens_python.recipe import saml, thirdparty +from supertokens_python.recipe.thirdparty import ( + ProviderClientConfig, + ProviderConfig, + ProviderInput, + SignInAndUpFeature, +) + + +init( + supertokens_config=SupertokensConfig( + connection_uri="", + api_key="", + ), + app_info=InputAppInfo( + app_name="App name", + api_domain="", + website_domain="", + ), + framework="fastapi", + recipe_list=[ + saml.init(), + thirdparty.init( + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + # Must start with "saml-" + third_party_id="saml-azure", + # Name shown on the login page + name="Azure SAML", + clients=[ + ProviderClientConfig( + # The client ID from step 3 + client_id="", + ) + ], + ) + ) + ] + ) + ), + ], +) +``` + + + +### 6. Test rejection paths before production + +Use an isolated test tenant and test IdP to verify that invalid SAML responses fail closed. At minimum, confirm that authentication is rejected for: + +- A response that has neither a valid response signature nor a valid signature on every assertion, including an assertion modified after signing and a response containing a duplicate or wrapping assertion. +- An assertion with a future `NotBefore` value or an expired `NotOnOrAfter` value. + +Core `12.1.1` is not expected to reject a mismatched `Response.Destination`, and its audience validation is insufficient for responses containing multiple assertions. If a trusted upstream validator supplies the missing controls, test destination and per-assertion audience rejection at that layer before production. + +Do not disable signature or time-condition validation to make negative tests pass. Keep failure details in server-side logs and return a generic authentication error to the browser. ## See also - - - - - - + + + + + + diff --git a/docs/authentication/enterprise/subdomain-login.mdx b/docs/authentication/enterprise/subdomain-login.mdx index d51162388f..284214d05e 100644 --- a/docs/authentication/enterprise/subdomain-login.mdx +++ b/docs/authentication/enterprise/subdomain-login.mdx @@ -1,23 +1,16 @@ --- title: Implement subdomain login -hide_title: true -sidebar_position: 4 description: Authenticate users across different tenants through different subdomains. -page_type: guide -recipe: thirdparty -category: enterprise-login +sidebar: + order: 50 --- - -# Subdomain login - -## Overview +## Overview This guide shows you how to authenticate users through different subdomains. -The authentication method that displayed on each page varies based on tenant configuration. +The authentication method displayed on each page varies based on the tenant configuration. -:::important -Throughout this page, assume that the tenant ID for a tenant is equal to their sub domain. If the sub domain assigned to a tenant is `customer1.example.com`, then their `tenantId` is `customer1`. +:::note[Throughout this page, assume that a tenant's ID matches its subdomain. If the subdomain assigned to a tenant is `customer1.example.com`, then its `tenantId` is `customer1`.] An example app for this setup with the **pre-built UI** is available on [the GitHub example directory](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-one-login-per-subdomain). The app is setup to have three tenants: - `tenant1.example.com`: Login with `emailpassword` + Google sign in @@ -30,49 +23,49 @@ An example app for this setup with the **pre-built UI** is available on [the Git The tutorial assumes that you already have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +If you have not, please check the [Quickstart Guide](/quickstart). Your application also needs you to create the tenants it requires. -View the [previous tutorial](/docs/authentication/enterprise/initial-setup) for more information on how to do this. +View the [previous tutorial](/authentication/enterprise/initial-setup) for more information on how to do this. ## Steps -### 1. Change CORS setting and `websiteDomain` + -:::warning no-title -You have to [create tenants](/docs/authentication/enterprise/initial-setup) before you can complete this step. +### 1. Change the CORS settings and `websiteDomain` + +:::warning +You have to [create tenants](/authentication/enterprise/initial-setup) before you can complete this step. ::: #### 1.1 CORS setup -In order for the browser to be able to make requests to the backend, the CORS setting on the backend needs to reflect the right set of allowed origins. -For example, if you have `customer1.example.com` on the frontend, then the CORS setting on the backend should allow `customer1.example.com` as an allowed origin. -You can specifically whitelist the set of frontend sub domains on the backend, or you can use a regex like `*.example.com`. +For browsers to make requests to the backend, configure backend CORS with the exact allowed origins. +For example, if the frontend uses `https://customer1.example.com`, allow that full origin. If tenants are dynamic, +validate the request's full `Origin` value against an anchored pattern that permits only your intended HTTPS subdomains. #### 1.2 `websiteDomain` setup Set the `websiteDomain` to `window.location.origin` in the frontend SDK initialization step. -On the backend, update the `websiteDomain` to be the main domain (`example.com` if your sub domains are `sub.example.com`). +On the backend, update `websiteDomain` to the main domain (`example.com` if your subdomains are `sub.example.com`). Then override the `sendEmail` functions to change the domain of the link dynamically based on the tenant ID supplied to the `sendEmail` function. See the Email Delivery section in the docs for how to override the `sendEmail` function. ### 2. Load login methods dynamically on the frontend based on the `tenantId` - - -Modify the `SuperTokens.init` to do the following: -1. Set the `usesDynamicLoginMethods` to true. This tells the frontend SDK that the login page relies on the `tenantId` and to fetch the tenant configuration from the backend before showing any login UI. -2. Initialize the `Multitenancy` recipe and provide `getTenantId` configuration function. + - +Modify `SuperTokens.init` as follows: +1. Set `usesDynamicLoginMethods` to `true`. This tells the frontend SDK that the login page relies on the tenant ID and must fetch the tenant configuration from the backend before showing any login UI. +2. Initialize the `Multitenancy` recipe and provide the `getTenantId` configuration function. - - + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; @@ -80,243 +73,214 @@ import Session from "supertokens-auth-react/recipe/session"; import Multitenancy from "supertokens-auth-react/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - // highlight-start - usesDynamicLoginMethods: true, - // highlight-end - recipeList: [ - // Other recipes.. - // highlight-start - Multitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: async () => { - // We treat the sub domain as the tenant ID - return window.location.host.split('.')[0] - } - } - } + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + usesDynamicLoginMethods: true, + recipeList: [ + // Other recipes.. + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: async () => { + // We treat the subdomain as the tenant ID + return window.location.host.split(".")[0]; }, - }) - // highlight-end - ] + }; + }, + }, + }), + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -import supertokensUIMultitenancy from "supertokens-auth-react-script/recipe/multitenancy"; supertokensUIInit({ appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", }, - // highlight-start usesDynamicLoginMethods: true, - // highlight-end recipeList: [ // Other recipes... supertokensUISession.init(), - // highlight-start supertokensUIMultitenancy.init({ override: { functions: (oI) => { return { ...oI, getTenantId: async () => { - // We treat the sub domain as the tenant ID - return window.location.host.split('.')[0] - } - } - } + // We treat the subdomain as the tenant ID + return window.location.host.split(".")[0]; + }, + }; + }, }, - }) - // highlight-end - ] + }), + ], }); ``` + + - - - + - + - -You can fetch the list of user's login methods based on their tenant ID (which you can derive from the current sub domain value) as shown below. +You can fetch the user's login methods based on their tenant ID, which you can derive from the current subdomain, as shown below. - - - - - + + + + ```tsx -import Multitenancy from 'supertokens-web-js/recipe/multitenancy'; +import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { - const loginMethods = await Multitenancy.getLoginMethods({ - tenantId - }) - - if (loginMethods.firstFactors.includes("thirdparty")) { - const providers = loginMethods.thirdParty.providers; - if (providers.find(i => i.id === "active-directory")) { - // render sign in with Active Directory button - } else { - // more checks for other providers - } - } else { - // thirdparty login is disabled for the tenant - } + const loginMethods = await Multitenancy.getLoginMethods({ + tenantId, + }); + + if (loginMethods.firstFactors.includes("thirdparty")) { + const providers = loginMethods.thirdParty.providers; + if (providers.find((i) => i.id === "active-directory")) { + // render sign in with Active Directory button + } else { + // more checks for other providers + } + } else { + // thirdparty login is disabled for the tenant + } } ``` - - - - + + ```tsx -import Multitenancy from 'supertokens-web-js/recipe/multitenancy'; +import Multitenancy from "supertokens-web-js/recipe/multitenancy"; async function fetchThirdPartyLoginProvidersForTenant(tenantId: string) { - const loginMethods = await Multitenancy.getLoginMethods({ - tenantId - }) - - if (loginMethods.firstFactors.includes("thirdparty")) { - const providers = loginMethods.thirdParty.providers; - if (providers.find(i => i.id === "active-directory")) { - // render sign in with Active Directory button - } else { - // more checks for other providers - } - } else { - // thirdparty login is disabled for the tenant - } + const loginMethods = await Multitenancy.getLoginMethods({ + tenantId, + }); + + if (loginMethods.firstFactors.includes("thirdparty")) { + const providers = loginMethods.thirdParty.providers; + if (providers.find((i) => i.id === "active-directory")) { + // render sign in with Active Directory button + } else { + // more checks for other providers + } + } else { + // thirdparty login is disabled for the tenant + } } ``` - - - - - - - - - - + + + + ```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/loginmethods' +curl --location --request GET '/auth/loginmethods' ``` + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: The `recipes` field contains information about which login methods are active along with the list of third party providers configured for this tenant. - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. + + - - - - -You also need to initialize the multi tenancy recipe with the following callback defined. You can get the value of `tenantId` from the sub domain as shown below. - - + + +You also need to initialize the multitenancy recipe with the following callback. You can get the tenant ID from the subdomain as shown below. + + +After you have shown the login methods and the user tries to sign in, follow all the steps for mobile app login similar to the [social login steps](/authentication/social/initial-setup#2-add-the-login-ui). When calling the sign in up API, also pass in the `tenantId` in the request path. An example of this appears below: + + + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Multitenancy from "supertokens-web-js/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - }, - recipeList: [ - // highlight-start - Multitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: async () => { - // We treat the sub domain as the tenant ID - return window.location.host.split('.')[0] - } - } - } + appInfo: { + appName: "...", + apiDomain: "...", + }, + recipeList: [ + Multitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: async () => { + // We treat the subdomain as the tenant ID + return window.location.host.split(".")[0]; }, - }) - // highlight-end - // other recipes... - ] + }; + }, + }, + }), + // other recipes... + ], }); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensMultitenancy from "supertokens-web-js-script/recipe/multitenancy"; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - }, - recipeList: [ - // highlight-start - supertokensMultitenancy.init({ - override: { - functions: (oI) => { - return { - ...oI, - getTenantId: async () => { - // We treat the sub domain as the tenant ID - return window.location.host.split('.')[0] - } - } - } + appInfo: { + appName: "...", + apiDomain: "...", + }, + recipeList: [ + supertokensMultitenancy.init({ + override: { + functions: (oI) => { + return { + ...oI, + getTenantId: async () => { + // We treat the subdomain as the tenant ID + return window.location.host.split(".")[0]; }, - }) - // highlight-end - // other recipes... - ] + }; + }, + }, + }), + // other recipes... + ], }); ``` - - - - - - - - - -After you have shown the login methods and the user tries to sign in, follow all the steps for mobile app login similar to the [social login steps](../../custom-ui/thirdparty-login). When calling the sign in up API, also pass in the `tenantId` in the request path. An example of this appears below: - - - + + + + ```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ +curl --location --request POST '/auth/signinup' \ --header 'Content-Type: application/json' \ --data-raw '{ "thirdPartyId": "...", @@ -327,55 +291,46 @@ curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signi }, }' ``` - - - - - - - - + + -### 3. Restrict subdomain access + -Restrict which sub domains the user has access to. -To do this configure the SDK to know which domain each `tenantId` has access to. +### 3. Restrict session use by subdomain - +Restrict the subdomains on which a tenant's sessions can be used. +To do this, configure the SDK with the domains for each tenant ID. - + + ```tsx import SuperTokens from "supertokens-node"; -import Multitenancy from "supertokens-node/recipe/multitenancy" +import Multitenancy from "supertokens-node/recipe/multitenancy"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - Multitenancy.init({ - getAllowedDomainsForTenantId: async (tenantId, userContext) => { - // query your db to get the allowed domain for the input tenantId - // or you can make the tenantId equal to the sub domain itself - return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"] - } - }), - // highlight-end - // other recipes... - ] -}) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Multitenancy.init({ + getAllowedDomainsForTenantId: async (tenantId, userContext) => { + // query your db to get the allowed domain for the input tenantId + // or you can make the tenantId equal to the subdomain itself + return [tenantId + ".myapp.com", "myapp.com", "www.myapp.com"]; + }, + }), + // other recipes... + ], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/multitenancy" @@ -386,23 +341,19 @@ import ( func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ - // highlight-start multitenancy.Init(&multitenancymodels.TypeInput{ GetAllowedDomainsForTenantId: func(tenantId string, userContext supertokens.UserContext) ([]string, error) { // query your db to get the allowed domain for the input tenantId - // or you can make the tenantId equal to the sub domain itself + // or you can make the tenantId equal to the subdomain itself return []string{tenantId + ".myapp.com", "myapp.com", "www.myapp.com"}, nil }, }), - // highlight-end }, }) } ``` - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import multitenancy @@ -428,211 +379,219 @@ init( ], ) ``` + + + +The configuration above tells SuperTokens to add the returned domains to the user's session claims when they sign in. +The SDK can access the claim on the frontend and backend to restrict where the session is used. + +:::warning[Domain checks are not tenant authorization] +`AllowedDomainsClaim` and `hasAccessToCurrentDomain` restrict session use by hostname. They do not prove that the user +belongs to an organization, enforce CORS or allowed browser origins, authorize access to business data, or provide +complete tenant isolation. After authentication, verify the user's tenant membership. On every business-data access, +the backend must derive the tenant from trusted session data and enforce application-level tenant authorization. Do +not trust a tenant ID, hostname, or claim supplied by the browser as authorization. +::: - - - - -The configuration above tells SuperTokens to add the list of domains returned by you into the user's session claims once they login. -This way the SDK accesses the claim on the frontend and backend to limit a user only to the right subdomains. - -### 4. Share sessions across sub domains {{optional}} +### 4. Share sessions across subdomains (optional) -If you need to allow users to have the same session across multiple subdomains you have to update the configuration. -Set the [`sessionTokenFrontendDomain` value ](/docs/post-authentication/session-management/share-session-across-sub-domains) in the `Session` recipe to enable this behavior. -If the sub domain and the main website domain have different backends (on different sub domains), then you can also enable [sharing of sessions across API domains](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). +If users need the same session across multiple subdomains, update the configuration. +Set the [`sessionTokenFrontendDomain` value ](/post-authentication/session-management/share-session-across-sub-domains) in the `Session` recipe to enable this behavior. +If the subdomain and main website domain have different backends on different subdomains, you can also enable [sharing of sessions across API domains](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints). -:::note -Even if they visit the main domain (logged in via `a.example.com`, and visit `example.com`), the frontend app there can detect if the user has a session or not. -This is not a security issue because access restricts users based on their domain allow list as shown below. +:::note[Even if they visit the main domain (logged in via `a.example.com`, and visit `example.com`), the frontend app there can detect if the user has a session or not.] +This only shows that a session exists. The domain validator below restricts where that session is used; application-level tenant authorization is still required. ::: -### 5. Limit user access to their sub domain. +### 5. Limit session use to the tenant's subdomain -Use [session claim validators](/docs/additional-verification/session-verification/claim-validation#using-session-claims) on the frontend to restrict sub domain access. +Use [session claim validators](/additional-verification/session-verification/claim-validation#using-session-claims) on the frontend to restrict session use by subdomain. Before proceeding, ensure that you have defined the `GetAllowedDomainsForTenantId` function mentioned above. This adds the list of allowed domains into the user's access token payload. -On the frontend, check if the tenant has access to the current sub domain. -If not, redirect them to the right sub domain. -This is possible by using the `hasAccessToCurrentDomain` session validator from the multi tenancy recipe. +On the frontend, check whether the current subdomain is in the session's allowed domains. +If it is not, redirect the user to the correct subdomain. +Use the `hasAccessToCurrentDomain` session validator from the multitenancy recipe. - + - + + +Make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import React from "react"; -import Session from 'supertokens-auth-react/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-auth-react/recipe/multitenancy'; +import Session from "supertokens-auth-react/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-auth-react/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](/docs/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). - - - - - -Make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -import supertokensMultitenancy from "supertokens-web-js-script/recipe/multitenancy"; supertokensUISession.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await supertokensUISession.getClaimValue({ - claim: supertokensMultitenancy.AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await supertokensUISession.getClaimValue({ + claim: supertokensMultitenancy.AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` - + + + + + +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy'; +import Session from "supertokens-web-js/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` + + -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](../sessions/protecting-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). - - + + +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. + + - + - - - - - - + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; -import { AllowedDomainsClaim } from 'supertokens-web-js/recipe/multitenancy'; +import Session from "supertokens-web-js/recipe/session"; +import { AllowedDomainsClaim } from "supertokens-web-js/recipe/multitenancy"; Session.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await Session.getClaimValue({ - claim: AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await Session.getClaimValue({ + claim: AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` - - - - - -```tsx -import supertokensSession from 'supertokens-web-js-script/recipe/session'; -import supertokensMultitenancy from 'supertokens-web-js-script/recipe/multitenancy'; + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." supertokensSession.init({ - override: { - functions: (oI) => ({ - ...oI, - getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ - ...claimValidatorsAddedByOtherRecipes, - { - ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), - onFailureRedirection: async () => { - let claimValue = await supertokensSession.getClaimValue({ - claim: supertokensMultitenancy.AllowedDomainsClaim, - }); - return "https://" + claimValue![0]; - }, - }, - ], - }), - }, -}) + override: { + functions: (oI) => ({ + ...oI, + getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => [ + ...claimValidatorsAddedByOtherRecipes, + { + ...supertokensMultitenancy.AllowedDomainsClaim.validators.hasAccessToCurrentDomain(), + onFailureRedirection: async () => { + let claimValue = await supertokensSession.getClaimValue({ + claim: supertokensMultitenancy.AllowedDomainsClaim, + }); + return "https://" + claimValue![0]; + }, + }, + ], + }), + }, +}); ``` + + - - - -Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever you check protect a route](../sessions/protecting-frontend-routes#verifying-the-claims-of-a-session--cust), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to their right sub domain (via the values set in the `AllowedDomainsClaim` session claim). +Above, in `Session.init` on the frontend, add the `hasAccessToCurrentDomain` claim validator to the global validators. This means that [whenever a route requires protection](/additional-verification/session-verification/protect-frontend-routes#check-the-claims-of-a-session), it checks if `hasAccessToCurrentDomain` has passed. If not, SuperTokens redirects the user to the correct subdomain using the values in the `AllowedDomainsClaim` session claim. - + --- ## See also - - - - - - + + + + + + diff --git a/docs/authentication/enterprise/tenant-discovery.mdx b/docs/authentication/enterprise/tenant-discovery.mdx index f413ca0442..ef62090052 100644 --- a/docs/authentication/enterprise/tenant-discovery.mdx +++ b/docs/authentication/enterprise/tenant-discovery.mdx @@ -1,13 +1,10 @@ --- title: Tenant discovery -hide_title: true -sidebar_position: 5 description: Automatically discover and route users to appropriate tenants based on their email domains -page_type: tutorial +sidebar: + order: 60 --- -# Tenant discovery - ## Overview This tutorial shows you how to add tenant discovery functionality to your SuperTokens authentication flows. @@ -15,13 +12,13 @@ The guide makes use of the plugins functionality which automatically discovers a ## How it works -The plugin extracts the domain from user email addresses and to infer the tenant ID. +The plugin extracts the domain from user email addresses to infer the tenant ID. For example, `user@company.com` would be routed to the `company` tenant. The system includes built-in protection against popular email providers and falls back to the `public` tenant when appropriate. ## Before you start -The tenant discovery plugin supports only the `React` and `NodeJS` SDKs. +The tenant discovery plugin supports only the React and Node.js SDKs. Support for other platforms is under active development. Besides initializing the plugin, you also have to configure multi-tenancy in your SuperTokens setup. @@ -43,7 +40,8 @@ import TenantDiscoveryPlugin from "@supertokens-plugins/tenant-discovery-nodejs" SuperTokens.init({ appInfo: { - // your app info + appName: "My app", + apiDomain: "https://api.example.com", }, recipeList: [ // your other recipes @@ -54,7 +52,7 @@ SuperTokens.init({ enableTenantListAPI: false, // Optional: defaults to false }), ], - } + }, }); ``` @@ -74,7 +72,9 @@ import TenantDiscoveryPlugin from "@supertokens-plugins/tenant-discovery-react"; SuperTokens.init({ appInfo: { - // your app info + appName: "My app", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ // your recipes @@ -86,7 +86,7 @@ SuperTokens.init({ extractTenantIdFromDomain: true, // Optional: defaults to true }), ], - } + }, }); ``` @@ -95,10 +95,19 @@ SuperTokens.init({ With this configuration, when a user tries to sign in, the system automatically determines their tenant based on the email domain. Hence, you don't need to change anything else to make it work. +:::warning[Tenant discovery is not tenant authorization] +Tenant discovery routes an authentication attempt to an inferred tenant. An email domain does not prove that the user +owns or belongs to an organization. Discovery does not enforce CORS or allowed browser origins, authorize access to +business data, or provide complete tenant isolation. After authentication, verify the user's membership in the +discovered tenant. On every business-data access, the backend must derive the tenant from trusted session data and +enforce application-level tenant authorization. Do not use the submitted email domain or a browser-supplied tenant ID +as authorization. +::: + Email authentication form @@ -109,13 +118,9 @@ If you want to customize the user interface experience the plugin also provides You can use the tenant selection interface accessible at `/tenant-discovery/select`. This page displays all available tenants and allows users to choose their organization before proceeding with authentication. -:::info -Keep in mind that you also need to enable the `tenant list` endpoint in your backend plugin configuration. +:::info[Keep in mind that you also need to enable the `tenant list` endpoint in your backend plugin configuration.] ::: -#### - - ## Customization ### Block emails from specific tenants @@ -141,13 +146,12 @@ TenantDiscoveryPlugin.init({ Extend the list of restricted domains that should always use the `public` tenant: -```typescript +```typescript check=false reason="This example omits surrounding application and SuperTokens configuration." TenantDiscoveryPlugin.init({ override: (originalImplementation) => ({ ...originalImplementation, isRestrictedEmailDomain: (emailDomain: string) => { - return originalImplementation.isRestrictedEmailDomain(emailDomain) || - emailDomain === "example.com"; + return originalImplementation.isRestrictedEmailDomain(emailDomain) || emailDomain === "example.com"; }, }), }); @@ -157,18 +161,20 @@ TenantDiscoveryPlugin.init({ To create a custom tenant discovery interface, use the `usePluginContext` hook: -```typescript +```tsx +import { useState } from "react"; import { usePluginContext } from "@supertokens-plugins/tenant-discovery-react"; function CustomTenantDiscovery() { const { api, functions } = usePluginContext(); const [email, setEmail] = useState(""); - const [tenants, setTenants] = useState([]); + const [tenants, setTenants] = useState>([]); const handleEmailSubmit = async () => { const result = await api.tenantIdFromEmail(email); if (result.status === "OK") { - functions.setTenantId(result.tenant, email, true); + functions.setEmailId(email); + functions.setTenantId(result.tenant); } }; @@ -182,22 +188,14 @@ function CustomTenantDiscovery() { return (

Enter your email to find your organization

- setEmail(e.target.value)} - placeholder="user@company.com" - /> + setEmail(e.target.value)} placeholder="user@company.com" /> - +

Or choose from available organizations:

{tenants.map((tenant) => ( - ))}
@@ -209,10 +207,10 @@ function CustomTenantDiscovery() { Besides tenant discovery, you can also explore other enterprise authentication features: - - - - - - - + + + + + + + diff --git a/docs/authentication/enterprise/tenant-management-plugin.mdx b/docs/authentication/enterprise/tenant-management-plugin.mdx index bce3f96f2f..8cd9959553 100644 --- a/docs/authentication/enterprise/tenant-management-plugin.mdx +++ b/docs/authentication/enterprise/tenant-management-plugin.mdx @@ -1,13 +1,10 @@ --- title: Tenant management -hide_title: true -sidebar_position: 6 description: Use the tenant management plugin for an out-of-the-box admin experience -page_type: tutorial +sidebar: + order: 80 --- -# Tenant management plugin - ## Overview This tutorial shows you how to add comprehensive tenant management functionality to your SuperTokens authentication flows. @@ -36,15 +33,14 @@ import TenantsPlugin from "@supertokens-plugins/tenants-nodejs"; SuperTokens.init({ appInfo: { - // your app info + appName: "My app", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { - plugins: [ - TenantsPlugin.init(), - ], + plugins: [TenantsPlugin.init()], }, }); ``` @@ -77,16 +73,15 @@ import TenantsPlugin from "@supertokens-plugins/tenants-react"; SuperTokens.init({ appInfo: { - // your app info + appName: "My app", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ - // your recipes + // your recipes ], experimental: { - plugins: [ - ProfileBasePlugin.init(), - TenantsPlugin.init(), - ], + plugins: [ProfileBasePlugin.init(), TenantsPlugin.init()], }, }); ``` @@ -103,7 +98,7 @@ The plugin supports the following configuration options: ### 3. Test the implementation With this configuration, users can access comprehensive tenant management features through the profile interface. -Make sure that you have a user with the required permissions and then access`/user/tenants/create` to create a new tenant. +Make sure that you have a user with the required permissions and then access`/user/tenants/create` to create a new tenant. ## Customization @@ -134,7 +129,8 @@ The plugin automatically creates the following roles and permissions: Configure custom email delivery for tenant-related notifications: -```typescript +```typescript check=false reason="This example omits the surrounding SuperTokens application configuration." +import SuperTokens from "supertokens-node"; import { PluginSMTPService } from "@supertokens-plugins/tenants-nodejs"; import TenantsPlugin from "@supertokens-plugins/tenants-nodejs"; @@ -168,7 +164,7 @@ SuperTokens.init({ You can override default behaviors by providing custom implementations: -```typescript +```typescript check=false reason="This example omits surrounding application and SuperTokens configuration." TenantsPlugin.init({ override: { functions: (originalImplementation) => ({ @@ -193,12 +189,13 @@ TenantsPlugin.init({ To create your own UI you can use the `usePluginContext` hook. It exposes an interface which you can use to interface with the endpoints exposed by the backend plugin. -```typescript -import { usePluginContext } from "@supertokens-plugins/tenants-react"; +```tsx +import { useState } from "react"; +import { usePluginContext } from "@supertokens-plugins/tenants-react/dist/plugin"; function CustomTenantComponent() { const { api, t } = usePluginContext(); - const [tenants, setTenants] = useState([]); + const [tenants, setTenants] = useState>([]); const handleFetchTenants = async () => { const result = await api.fetchTenants(); @@ -207,14 +204,14 @@ function CustomTenantComponent() { } }; - const handleCreateTenant = async (name) => { + const handleCreateTenant = async (name: string) => { const result = await api.createTenant({ name }); if (result.status === "OK") { console.log("Tenant created successfully"); } }; - const handleSwitchTenant = async (tenantId) => { + const handleSwitchTenant = async (tenantId: string) => { const result = await api.switchTenant(tenantId); if (result.status === "OK") { console.log("Switched to tenant successfully"); @@ -223,14 +220,14 @@ function CustomTenantComponent() { return (
-

{t("PL_TD_SELECT_TENANT_TITLE")}

+

{t("PL_TB_CREATE_TENANT_LABEL")}

{tenants.map((tenant) => (
- {tenant.name} ({tenant.role}) - + + {tenant.name} ({tenant.role}) + +
))}
@@ -242,7 +239,7 @@ function CustomTenantComponent() { You can customize the default pages by providing your own components: -```typescript +```typescript check=false reason="This example depends on local application modules." import TenantsPlugin from "@supertokens-plugins/tenants-react"; import { CustomSelectTenant, CustomTenantManagement } from "./your-custom-components"; @@ -269,10 +266,10 @@ SuperTokens.init({ Besides tenant management, you can also explore other enterprise authentication features: - - - - - - - + + + + + + + diff --git a/docs/authentication/m2m/_category_.json b/docs/authentication/m2m/_category_.json deleted file mode 100644 index 274c8ee4f0..0000000000 --- a/docs/authentication/m2m/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Machine to Machine", - "position": 7 -} diff --git a/docs/authentication/m2m/client-credentials.mdx b/docs/authentication/m2m/client-credentials.mdx index 6bd7df5667..f2b7a0e441 100644 --- a/docs/authentication/m2m/client-credentials.mdx +++ b/docs/authentication/m2m/client-credentials.mdx @@ -1,36 +1,33 @@ --- title: Client Credentials Flow -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 -description: >- - Configure OAuth2 client credentials flow for microservices authentication and - access token verification. -page_type: tutorial -recipe: oauth2 -category: machine-to-machine +description: Configure OAuth2 client credentials flow for microservices authentication and access token verification. +sidebar: + order: 2 --- - -import CreateOAuth2ClientRequest from "../unified-login/_blocks/create-oauth2-client-request-m2m.mdx"; - -# Client credentials authentication - ## Overview In the **Client Credentials Flow** the authentication sequence works in the following way: + + + + + + + -## `Service A` uses credentials to get an **OAuth2 Access Token** -## [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) returns the **OAuth2 Access Token** -## `Service A` uses the **OAuth2 Access Token** to communicate with `Service B` -## `Service B` validates the **OAuth2 Access Token** -## If the token is valid `Service B` returns the requested resource + + + + + + -Machine to Machine Authentication +Machine to Machine Authentication Before going into the actual instructions, start by imagining a real life example that you can reference along the way. This makes it easier to understand what is happening. @@ -38,17 +35,14 @@ This makes it easier to understand what is happening. We are going to configure authentication for the following setup: - A **Calendar Service** that exposes these actions: `event.view`, `event.create`, `event.update` and `event.delete` - A **File Service** that exposes these actions: `file.view`, `file.create`, `file.update` and `file.delete` -- A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task +- A **Task Service** that interacts with the **Calendar Service** and the **File Service** in the process of scheduling a task The aim is to allow the **Task Service** to perform an authenticated action on the **Calendar Service**. Proceed to the actual steps. ## Before you start - - -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page, please follow the tutorial and return here once you're done. + ## Steps @@ -61,82 +55,96 @@ You should be able to use the OAuth2 recipes in your applications. ### 2. Create the OAuth2 Clients -For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). +For each of your **`microservices`** you need to create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). This can occur by directly calling the **SuperTokens Core** API. - +For manual curl testing, provision a config through your secret-management or deployment system, restrict it to the +service account with mode `0600`, and do not commit it: + +```text +header = "api-key: " +``` -:::info Custom Example +The cURL example refers to this file as ``. This keeps the API key out of shell history and +process arguments. Disable shell tracing and curl verbose or trace output, and ensure HTTP, process, and error logs do not +record request headers, config contents, or the API key. + +See the [Create OAuth2 client API reference](/references/cdi/oauth2provider-recipe/createoauth2client) for the complete +request schema and response details. + +", + clientName: "", + grantTypes: ["client_credentials"], + scope: " ", + audience: [""], + }} +/> + +:::info[Custom Example] To create a client for the **Task Service**, use the following attributes: ```json - { - "clientName": "Task Service", - "grantTypes": ["client_credentials"], - "scope": "event.view event.create event.edit event.delete file.view file.create file.edit file.delete", - "audience": ["event", "file"] - } +{ + "clientId": "task-service", + "clientName": "Task Service", + "grantTypes": ["client_credentials"], + "scope": "event.view event.create event.update event.delete file.view file.create file.update file.delete", + "audience": ["event", "file"] +} ``` This allows the **Task Service** to perform all types of actions against both of the other services as long as it has a valid **OAuth2 Access Token**. ::: -:::caution -You have to save the create response because this is not persisted internally for security reasons. -The information is necessary for the next steps. +:::note[Retry client provisioning safely] +Client creation has no documented duplicate-request key. Use a stable `clientId`, serialize provisioning for that client, and +after a timeout query the client by ID before retrying. Do not blindly retry an uncertain `POST` response. ::: +:::warning[Protect the client credentials] +Store the client ID and secret in a secret manager. The Core persists the secret encrypted at rest, and callers with the +Core API key can retrieve it. Treat both the API key and client secret as sensitive credentials. +::: -### 3. Set Up your Authorization Service - -In your [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend, initialize the **OAuth2Provider** recipe. - - - +### 3. Set Up your Authorization Service -Update the `supertokens.init` call to include the new recipe. +The Node.js and Python SDKs automatically initialize the **OAuth2Provider** recipe when it is absent. Add it explicitly to +your [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) configuration when you +need recipe overrides or want to make the dependency visible. + + ```tsx import supertokens from "supertokens-node"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "...", - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - OAuth2Provider.init(), - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [OAuth2Provider.init()], }); ``` - - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. -You can use the [legacy method](/docs/microservice_auth/legacy/implementation-guide) to authenticate microservices based on your language. - -::: - - - - - -```python + + +```python from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider +from supertokens_python.recipe import oauth2provider init( app_info=InputAppInfo( @@ -154,44 +162,50 @@ init( ], ) ``` + + - +### 4. Generate access tokens - -### 4. Generate access tokens +You can directly call the [**Authorization Server**](/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens. +See the [Exchange OAuth grant API reference](/references/fdi/oauth2provider-recipe/oauthtokenpost) for response schemas and +error details. The cURL example remains authoritative for this request because the current FDI specification does not model +the form-encoded request body or HTTP Basic client authentication. +Keep the client secret out of command arguments and shell history. For manual testing, provision a curl config through +your secret-management or deployment system, restrict it to the service account with mode `0600`, and do not commit it: - +```text +user = ":" +``` -You can directly call the [**Authorization Server**](/docs/authentication/unified-login/oauth2-basics#authorization-server) to generate Access Tokens. -Check the following code snippet to see how you can do that: +Then reference the protected config by path: ```bash -curl -X POST ^{appInfo.apiDomain}^{appInfo.apiBasePath}/oauth/token \ --H "Content-Type: application/json" \ --d '{ - "client_id": "", - "client_secret": "", - "grant_type": "client_credentials", - "scope": [""], - "audience": "" -}' +curl -X POST '/auth/oauth/token' \ + --config '' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'scope=' \ + --data-urlencode 'audience=' ``` +For production, load the secret from a secret manager in your application client. Disable shell tracing and ensure HTTP, +process, and error logs do not record authorization headers, curl configuration contents, or client secrets. + You should limit the scopes that you are requesting to the ones necessary to perform the desired action. -:::info Custom Example +:::info[Custom Example] If the **Task Service** wants to create an event on the **Calendar Service**, a token with the following attributes needs generation: -```json - { - "client_id": "", - "client_secret": "", - "grant_type": "client_credentials", - "scope": ["event.create"], - "audience": "event" - } +```bash +curl -X POST '/auth/oauth/token' \ + --config '' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=client_credentials' \ + --data-urlencode 'scope=event.create' \ + --data-urlencode 'audience=event' ``` ::: @@ -200,8 +214,10 @@ The **Authorization Server** returns a response that looks like this: ```json { - "access_token": "", - "expires_in": 3600 + "access_token": "", + "expires_in": 3600, + "token_type": "bearer", + "scope": "event.create" } ``` @@ -215,491 +231,146 @@ Keep in mind to generate a new one when it expires. ### 5. Verify an OAuth2 Access Token -To check the validity of a token, use a generic **JWT** verification library. - -Besides the standard **OAuth2** token claims, the implementation includes an additional one called `stt`. -This stands for `SuperTokens Token Type`. -It ensures that the validation occurs for the correct token type: -- `0` represents a **SuperTokens Session Access Token** -- `1` represents an **OAuth2 Access Token** -- `2` represents an **OAuth2 ID Token**. - - - - - -For NodeJS you can use [`jose`](https://github.com/panva/jose) to verify the token. +Use the released SuperTokens backend SDK validator instead of implementing JWT validation yourself. It validates the +signature, expiration, and `stt=1` token type. Pass requirements for the intended audience, client, and every scope needed +by the operation. Also compare the token issuer with your Authorization Server's issuer. + + ```tsx -import * as jose from "jose"; - -const JWKS = jose.createRemoteJWKSet(new URL('^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json')) - -async function validateClientCredentialsToken(jwt: string) { - const requiredScope = ""; - const audience = ''; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +async function validateClientCredentialsToken(token: string): Promise { try { - const { payload } = await jose.jwtVerify(jwt, JWKS, { - audience, - requiredClaims: ['stt', 'scp'], + const result = await OAuth2Provider.validateOAuth2AccessToken(token, { + audience: "", + clientId: "", + scopes: [""], }); - if(payload.stt !== 1) return false; - - const scopes = payload.scp as string[]; - return scopes.includes(requiredScope); - } catch (err) { + return result.payload.iss === "/auth"; + } catch { return false; } } ``` - - - - - -You can use the [`jwx`](https://github.com/lestrrat-go/jwx) library to verify the token. - -```go -import ( - "context" - "fmt" - - "github.com/lestrrat-go/jwx/jwt" - "github.com/lestrrat-go/jwx/jwk" -) - -func ValidateToken(token string) bool { - apiDomain := "^{appInfo.apiDomain}" - apiBasePath := "^{appInfo.apiBasePath}" - requiredScope := "" - - jwksURL := fmt.Sprintf("%s%sjwt/jwks.json", apiDomain, apiBasePath) - jwks, err := jwk.Fetch(context.Background(), jwksURL) - if err != nil { - return false - } - - parsedToken, err := jwt.Parse( - []byte(token), - jwt.WithKeySet(jwks), - jwt.WithClaimValue("stt", 1), - jwt.WithAudience(""), - ) - if err != nil { - return false - } - - scp, ok := parsedToken.Get("scp") - if !ok { - return false - } - - scopes, ok := scp.([]interface{}) - if !ok { - return false - } - - for _, scope := range scopes { - if scope, ok := scope.(string); ok && scope == requiredScope { - return true - } - } - - return false; -} -``` - - - - - -You can use the [PyJWT](https://github.com/jpadilla/pyjwt) library to verify the token. - -```python -from typing import Optional, List -import jwt -from jwt import PyJWKClient - -def validate_token(token: str) -> bool: - api_domain = "^{appInfo.apiDomain}" - api_base_path = "^{appInfo.apiBasePath}" - audience = "" - required_scope = "" - - jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json" - jwks_client = PyJWKClient(jwks_url) - - try: - signing_key = jwks_client.get_signing_key_from_jwt(token) - decoded = jwt.decode( - token, - signing_key.key, - algorithms=['RS256'], - audience=audience, - options={"require": ["stt", "scp"]} - ) - - stt: Optional[int] = decoded.get('stt') - if stt != 1: - return False - - scopes: List[str] = decoded.get('scp', []) - if required_scope not in scopes: + + +```python +from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements +from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token + + +def validate_client_credentials_token(token: str) -> bool: + try: + result = validate_oauth2_access_token( + token=token, + requirements=OAuth2TokenValidationRequirements( + audience="", + client_id="", + scopes=[""], + ), + ) + return result.payload.get("iss") == "/auth" + except Exception: return False - - return True - except Exception: - return False -``` - - - - - -You can use the [Firebase JWT](https://github.com/firebase/php-jwt) library to verify the token. - -```php -require 'vendor/autoload.php'; - -use Firebase\JWT\JWT; -use Firebase\JWT\Key; - -function validateToken($jwt) { - $apiDomain = "^{appInfo.apiDomain}"; - $apiBasePath = "^{appInfo.apiBasePath}"; - $jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json'; - $requiredScope = ""; - $audience = ""; - - $jwks = json_decode(file_get_contents($jwksUrl), true); - try { - $decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256')); - if ($decoded->aud !== $audience) { - return false; - } - if ($decoded->sst !== 1) { - return false; - } - return in_array($requiredScope, $decoded->scp); - } catch (Exception $e) { - return false; - } -} -``` - - - - - -You can use the [Auth0 JWT](https://github.com/auth0/java-jwt) library to verify the token. - -```java -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.jwt.interfaces.JWTVerifier; -import com.auth0.jwt.JWTVerifier.Base; -import com.auth0.jwt.algorithms.Algorithm; - -import java.net.URL; -import java.util.Map; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; - -public class JWTVerifier { - - private static final String JWKS_URL = "^{appInfo.apiDomain}^{appInfo.apiBasePath}jwt/jwks.json"; - private static final String AUDIENCE = ""; - - private static Map fetchJWKS() throws Exception { - URL url = new URL(JWKS_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - InputStream responseStream = connection.getInputStream(); - Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name()); - String responseBody = scanner.useDelimiter("\\A").next(); - scanner.close(); - - return JWT.decode(responseBody).getHeader(); - } - - public static boolean validateToken(String token) { - try { - Map jwks = fetchJWKS(); - - Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null); - JWTVerifier verifier = JWT.require(algorithm) - .withAudience(AUDIENCE) - .build(); - - DecodedJWT jwt = verifier.verify(token); - if(jwt.getClaim("sst").asInt() != 1) { - return false; - } - List scopes = jwt.getClaim("scp").asList(); - return scopes.contains(requiredScope); - } catch (Exception e) { - return false; - } - } -} ``` + + + +:::warning[Bearer tokens do not prevent request replay] +Token validation authenticates and authorizes a request; it does not make a state-changing operation replay-safe. For +create or update APIs, require an application-level unique request key, atomically bind it to the authenticated client, +operation, and request-body digest, and return the stored result for an exact retry. Reject reuse with a different payload +and use business uniqueness or conditional updates where appropriate. +::: - - - - -You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library to verify the token. - -```csharp -using System; -using System.Linq; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; - -class ClientCredentialsTokenValidator -{ - static async Task ValidateToken(string jwtStr) - { - string apiDomain = ""; - string apiBasePath = ""; - string audience = ""; - string requiredScope = ""; - - HttpClient client = new HttpClient(); - var response = await client.GetStringAsync($"//jwt/jwks.json"); - var jwks = new JsonWebKeySet(response); - - var tokenHandler = new JwtSecurityTokenHandler(); - var validationParameters = new TokenValidationParameters - { - ValidAudience = audience, - IssuerSigningKeys = jwks.Keys - }; - - try - { - SecurityToken validatedToken; - var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken); - var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value); - - if (!claims.ContainsKey("stt") || claims["stt"] != "1") - { - return false; - } - - var scopes = claims["scp"].Split(" "); - if (!scopes.Contains(requiredScope)) - { - return false; - } - - return true; - } - catch (Exception) - { - return false; - } - } -} -``` - - - - - -:::info Custom Example +:::info[Custom Example] -If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** needs to check the following: -- Set the `stt` claim to `1` -- The `scp` claim contains `event.create` -- Set the `aud` claim to `event` +If the **Task Service** uses the previously generated token to create a calendar event, the **Calendar Service** must +require `stt=1`, the `event.create` scope, the `event` audience, the expected Task Service client ID, and the expected +Authorization Server issuer. ::: #### Handle both SuperTokens session tokens and OAuth2 access tokens -If you are using your **Authorization Service** also as a **Resource Server**, account for this in the way you verify the sessions. - -This is necessary because two types of tokens are in use: -- **SuperTokens Session Access Token**: Used during the login/logout flows. -- **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. - -Hence, a way to distinguish between these two and prevent errors is necessary. - - - - +If your Authorization Server is also a Resource Server, a protected route may accept either a SuperTokens session or an +OAuth2 access token. Parse the `Authorization` header strictly. Never accept a malformed bearer value, and never ignore a +validator's failure or false result. + + ```tsx -import supertokens from "supertokens-node"; +import express, { type NextFunction, type Request, type Response } from "express"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import Session from "supertokens-node/recipe/session"; -import express, { Request, Response, NextFunction } from 'express'; -import * as jose from "jose"; -async function verifySession(req: Request, res: Response, next: NextFunction) { - let session = undefined; - try { - session = await Session.getSession(req, res, { sessionRequired: false }); - } catch (err) { - if ( - !Session.Error.isErrorFromSuperTokens(err) || - err.type !== Session.Error.TRY_REFRESH_TOKEN - ) { - return next(err); - } - } +async function verifySessionOrOAuthToken(req: Request, res: Response, next: NextFunction) { + const authorization = req.headers.authorization; - // In this case we are dealing with a SuperTokens Session that has been validated - if (session !== undefined) { - return next(); - } - - // The OAuth2 Access Token needs to be manually extracted and validated - let jwt: string | undefined = undefined; - if (req.headers["authorization"]) { - jwt = req.headers["authorization"].split("Bearer ")[1]; - } - if (jwt === undefined) { - return next(new Error("No JWT found in the request")); + if (authorization !== undefined) { + try { + const result = await OAuth2Provider.validateOAuth2AccessToken(match[1], { + audience: "", + clientId: "", + scopes: [""], + }); + if (result.payload.iss === "/auth") { + return next(); + } + } catch { + // The bearer token may be a SuperTokens session access token. + } } try { - await validateToken(jwt); + await Session.getSession(req, res); return next(); - } catch (err) { - return next(err); + } catch { + return res.status(401).json({ message: "Unauthorized" }); } } -const JWKS = jose.createRemoteJWKSet( - new URL("^{appInfo.apiDomain}^{appInfo.apiBasePath}jwt/jwks.json"), -); - -// This is a basic example on how to validate an OAuth2 Token -// Use the previous example to extend it -async function validateToken(jwt: string) { - const { payload } = await jose.jwtVerify(jwt, JWKS, { - requiredClaims: ["stt", "scp", "sub"], - }); - - if (payload.stt !== 1) throw new Error("Invalid token"); - - // If the Authorizaton Server will handle different types of Authorization Flows - // You can differentiate between the different types of tokens by checking the `sessionHandle` claim - const sessionHandle = payload['sessionHandle'] as string | undefined; - if(sessionHandle === undefined) { - // We are dealing with a Client Credentials Token - // You can perform microservice authentication checks here - } else { - // Here we are validating tokens that have been generated in the Authorization Code Flow - } -} - -// You can then use the function as a middleware for a protected route const app = express(); -app.get("/protected", verifySession, async (req, res) => { - // Custom logic +app.get("/protected", verifySessionOrOAuthToken, async (_req, res) => { + res.json({ message: "Authorized" }); }); ``` - - - - - + + ```python -from supertokens_python.recipe.session.syncio import get_session -from supertokens_python.recipe.session.exceptions import SuperTokensSessionError, TryRefreshTokenError +from fastapi import HTTPException from fastapi.requests import Request -from typing import List, Optional -import jwt -from jwt import PyJWKClient - - -def verify_session(request: Request): - session = None - try: - session = get_session(request) - except SuperTokensSessionError as err: - if not isinstance(err, TryRefreshTokenError): - raise err - - - # In this case we are dealing with a SuperTokens Session - if session is not None: - return True - - - # The OAuth2 Access Token needs to be manually extracted and validated - jwt = None - auth_header = request.headers.get("authorization") - if auth_header: - # Split the Authorization header and get the token - parts = auth_header.split("Bearer ") - if len(parts) > 1: - jwt = parts[1] - if jwt is None: - raise ValueError("No JWT found in the request") - - validate_token(jwt, ''); - return True - - -def validate_token(token: str, required_scope: str) -> bool: - api_domain = "" - api_base_path = "/auth" - client_id = "" - - jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json" - jwks_client = PyJWKClient(jwks_url) - - try: - signing_key = jwks_client.get_signing_key_from_jwt(token) - decoded = jwt.decode( - token, - signing_key.key, - algorithms=['RS256'], - options={"require": ["stt", "client_id", "scp"]} - ) - - stt: Optional[int] = decoded.get('stt') - if stt != 1: - return False - - token_client_id: Optional[str] = decoded.get('client_id', None) - if client_id != token_client_id: - return False - - - scopes: List[str] = decoded.get('scp', []) - if required_scope not in scopes: - return False - - return True - except Exception: - return False - # - -``` - - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. - -::: - - +from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements +from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token +from supertokens_python.recipe.session.syncio import get_session - +def verify_session_or_oauth_token(request: Request) -> bool: + authorization = request.headers.get("authorization") + + if authorization is not None: + try: + result = validate_oauth2_access_token( + token=match.group(1), + requirements=OAuth2TokenValidationRequirements( + audience="", + client_id="", + scopes=[""], + ), + ) + if result.payload.get("iss") == "/auth": + return True + except Exception: + # The bearer token may be a SuperTokens session access token. + pass + + try: + get_session(request) + return True + except Exception as error: + raise HTTPException(status_code=401, detail="Unauthorized") from error +``` + + diff --git a/docs/authentication/m2m/introduction.mdx b/docs/authentication/m2m/introduction.mdx index 3b28728a7f..dea05f8645 100644 --- a/docs/authentication/m2m/introduction.mdx +++ b/docs/authentication/m2m/introduction.mdx @@ -1,18 +1,16 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true -description: >- - Authenticate microservices using SuperTokens with the OAuth2 Client - Credentials Flow. -page_type: overview -recipe: oauth2 -category: machine-to-machine +description: Authenticate microservices using SuperTokens with the OAuth2 Client Credentials Flow. +sidebar: + order: 1 --- - -# Machine to machine authentication + +Implement SuperTokens machine-to-machine authentication for this repository. Inspect the services, backend SDK, deployment model, and existing secrets configuration first. Prefer the managed-service OAuth2 client-credentials flow when supported; otherwise explain the legacy-flow tradeoff. Configure the OAuth2 provider, clients, token acquisition, service authentication, scopes, and secret storage without committing credentials. Validate token issuance, expiry, invalid credentials, and protected service-to-service requests. + ## Overview @@ -21,16 +19,16 @@ You have to create an **OAuth2 Provider** and use the **OAuth2 Client Credential ## Prerequisites - + -Before you can dive deeper in the functionality there are a few things to keep in mind: +Before you can dive deeper in the functionality there are a few things to keep in mind: - The feature is available with the **SuperTokens Managed Service**. It is not included in the **Self-Hosted** version. - You can use it with the `Node.js` or the `Python` backend SDKs. -If you do not meet the previous requirements you can use the [legacy flow](/docs/authentication/m2m/legacy-flow). +If you do not meet the previous requirements you can use the [legacy flow](/authentication/m2m/legacy-flow). -## Getting started +## Getting started Two separate quickstart guides are available for you to follow. The recommendation is to use the **Client Credentials Flow**. @@ -39,31 +37,14 @@ The **Legacy Flow** guide is here for backwards compatibility and it is going to Before you explore a guide, read through the **OAuth2 Basics** page first. It explains concepts used in each tutorial. - - - - OAuth2 Basics
-
- - Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. - -
- - - Client Credentials Flow - - - Implement a common authentication service that all your microservices can use. - - - - - Legacy Flow - - - Legacy info - - -
- - + + +Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. + + +Implement a common authentication service that all your microservices can use. + + +Legacy info + + diff --git a/docs/authentication/m2m/legacy-flow.mdx b/docs/authentication/m2m/legacy-flow.mdx index 9c4b1b81dd..639836cf5a 100644 --- a/docs/authentication/m2m/legacy-flow.mdx +++ b/docs/authentication/m2m/legacy-flow.mdx @@ -1,110 +1,80 @@ --- title: Legacy Flow -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 -description: >- - Guide for implementing JWT-based authentication between microservices using - SuperTokens Core. -page_type: tutorial -recipe: oauth2 -category: machine-to-machine +description: Guide for implementing custom JWT authentication between microservices using SuperTokens Core. +sidebar: + order: 3 --- - -# Legacy flow - ## Overview -This custom flow makes use of the **SuperTokens Core** without adhering to the **OAuth2** standard. -It involves creating private access tokens and passing them to the microservices. +Use the [OAuth2 Client Credentials Flow](/authentication/m2m/client-credentials) when it is available. It gives each +client an identity and uses the standard OAuth2 token and scope model. + +This legacy flow is a custom bearer-token scheme for deployments that cannot use client credentials. A service with +access to the SuperTokens Core JWT API can mint a token containing arbitrary claims. Consequently, a `source` or +`sub` claim proves only that a caller with signing access asserted that value; it does not independently prove which +service made the request. -The authentication sequence works in the following way: +The flow is: - ## Microservice `M1` requests a JWT (JSON Web Token) from the **SuperTokens Core** - ## Microservice `M1` sends the JWT to `M2` - ## Microservice `M2` verifies the JWT and completes the action if the JWT is valid + + + + + + -Microservice auth flow diagram - -The first step is to create a JWT from the microservice that sends the request (refer to this microservice as `M1`). -Other microservices verify this JWT when `M1` sends them a request. -Since this JWT remains static per microservice, the best time to create this is on process starts - that is when `M1` starts. - -The JWT can contain any information you like. At a minimum, it needs to contain information proving that it is a microservice allowed to query other microservices in your infrastructure. -This is necessary since you may issue a JWT to an end user as well, and they should not be able to query any microservice directly. -Add the following claim in the JWT to "mark" the JWT as one meant for microservice auth only: -```json -{..., "source": "microservice", ...} -``` -In the receiving microservice (`M2`), verify the JWT and check that this claim is present before serving the request. - - -### Security considerations - -#### Who can query the microservices? -Anyone or any service that has direct access to the SuperTokens core can produce a valid JWT and query your microservices. -If the core is open to the internet, you *must* add an API key to protect it. -Even though end users may receive a JWT for their session (that the core signs), they cannot query a microservice directly. Their JWT *should* not have the `source: "microservice"` claim in it. - -#### What happens if someone compromises the core's API key? - -Then the attacker can issue their own JWTs to be able to query your microservices. To limit this protection, you may want to add firewall rules to allow access to the core only from services on your backend. -You can also provide multiple API keys to the core and give a unique key to each microservice in your infrastructure. This way, it would be easier to track where a leak came from. - -#### What happens if someone compromises the JWT signing key? - -In this case, the attacker could fabricate their own JWT to be able to query your microservices. -To limit this risk, a JWT signing key rotation methodology is in place. Until then, you can limit the reachability of your microservices based on the request's IP address. - -#### How to limit which microservice can query another one? - -If an organisation has multiple teams and microservices, it is common to limit which other services a given microservice can query. -For example, if there exists `M1`, `M2` and `M3` microservices, there may be a situation in which `M1` should only be able to query `M2` and not `M3`. -With one SuperTokens core deployment, having this type of restriction is impossible. All the microservices create and verify their JWTs using the same public/private keys. Therefore `M3` receives a request, it has no way of reliably knowing if the request is from `M1` or `M2` (assuming that IP-based access control is not implemented). -This type of restriction can occur by deploying multiple cores connected to their own databases. In this example, a dedicated SuperTokens core can handle `M3`'s auth, such that only `M3` uses that to verify the incoming JWTs. Then, only other services that have access to that core can create JWTs that `M3` accepts. If `M1` doesn't have access to `M3`'s core's API key, it can be assured that successful requests to `M3` are not from `M1`. - - -## Steps +:::warning[Security boundary] +Anyone who can call the Core JWT API can mint any service identity or permission accepted by this scheme. Restrict +Core access with network controls and an API key, store the API key in a secrets manager, and monitor issuance. Multiple +Core API keys simplify secret rotation and may help attribute Core requests, but the issued JWT does not identify which +API key was used. Multiple keys therefore do not create cryptographic service identities or authorization boundaries. +::: -### 1. Create a JWT +For stronger isolation, use client credentials or separate trust domains. Deploying separate Cores can create separate +signing domains, but it adds operational cost and does not turn a shared Core API key into service identity. - +## Token policy - +For every token: -First, initialize the `JWT` recipe in the `supertokens.init` function call: +- Use a short validity appropriate to the request path. The examples below use five minutes. +- Use dynamic signing keys. Dynamic keys rotate every 168 hours (one week) by default unless the Core configuration + changes `access_token_dynamic_signing_key_update_interval`. +- Require an exact issuer (`iss`), audience (`aud`), subject/service identity (`sub`), source, token type, permissions, + and expiration (`exp`) at the receiving service. +- Grant only the permissions needed by the target API. Do not treat successful signature verification as authorization. +- Fetch keys from JWKS and support key rotation. Do not embed a public key in the application. - +The JWT recipe defaults to a 100-year validity and a static signing key when those arguments are omitted. Those defaults +are unsuitable for bearer credentials. Static keys do not rotate. Always pass a short validity and explicitly select +the dynamic signing key as shown below. - +## 1. Initialize the JWT recipe + + ```tsx -import supertokens from "supertokens-node" -import jwt from "supertokens-node/recipe/jwt" +import supertokens from "supertokens-node"; +import jwt from "supertokens-node/recipe/jwt"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", // location of the core - apiKey: "..." // provide the core's API key if configured - }, - recipeList: [ - // highlight-next-line - jwt.init() - ] -}) + appInfo: { + apiDomain: "https://auth.example.com", + appName: "service-auth", + websiteDomain: "https://example.com", + }, + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + recipeList: [jwt.init()], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/jwt" @@ -113,581 +83,260 @@ import ( func main() { supertokens.Init(supertokens.TypeInput{ - AppInfo: supertokens.AppInfo{ - AppName: "...", - WebsiteDomain: "...", - APIDomain: "...", - }, - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "...", // location of the core - APIKey: "...", // provide the core's API key if configured - }, + AppInfo: supertokens.AppInfo{ + AppName: "service-auth", + WebsiteDomain: "https://example.com", + APIDomain: "https://auth.example.com", + }, + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "...", + APIKey: "...", + }, RecipeList: []supertokens.Recipe{ - // highlight-next-line jwt.Init(nil), }, }) } ``` - - - - + + ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import jwt init( app_info=InputAppInfo( - app_name="...", - api_domain="...", - website_domain="...", + app_name="service-auth", + api_domain="https://auth.example.com", + website_domain="https://example.com", ), supertokens_config=SupertokensConfig( - connection_uri="...", # location of the core - api_key="..." # provide the core's API key if configured + connection_uri="...", + api_key="...", ), - framework='django', - recipe_list=[ - # highlight-next-line - jwt.init(), - ], + framework="django", + recipe_list=[jwt.init()], ) ``` + + - - +The `apiDomain`/`api_domain`/`APIDomain` value becomes the JWT issuer domain and must be the domain that serves the JWKS +endpoint. If this process initializes no other recipe, `appName` and `websiteDomain` do not affect this flow. -:::important -- The value of `apiDomain` should be the domain part of the JWKS URL used to verify the JWT (from `M2`). This should ideally be the domain of the microservice that has all the other SuperTokens' recipes initialized in them. -- If this microservice does not initialize any other recipe, the values of `appName` and `websiteDomain` don't matter. -::: +## 2. Create a short-lived JWT -After this, you can use the JWT recipe to create your own JWT whenever required: - - - +Use a fixed schema rather than accepting arbitrary claims from request input. This example identifies `M1`, limits the +token to `M2`, and grants one permission. + + ```tsx -import jwt from "supertokens-node/recipe/jwt" - -async function createJWT(payload: any) { - let jwtResponse = await jwt.createJWT({ - ...payload, - source: "microservice" - }); - if (jwtResponse.status === "OK") { - // Send JWT as Authorization header to M2 - return jwtResponse.jwt; - } - throw new Error("Unable to create JWT. Should never come here.") -} -``` +import jwt from "supertokens-node/recipe/jwt"; + +async function createServiceAccessToken(): Promise { + const response = await jwt.createJWT( + { + iss: "https://auth.example.com", + aud: "service-m2", + sub: "service-m1", + source: "microservice", + token_type: "service_access", + permissions: ["comments:write"], + }, + 300, + false, + ); - - + if (response.status !== "OK") { + throw new Error("JWT creation failed"); + } + return response.jwt; +} +``` + + ```go import ( - "fmt" + "errors" "github.com/supertokens/supertokens-golang/recipe/jwt" ) -func main() { - jwtResponse, err := jwt.CreateJWT(map[string]interface{}{ - "source": "microservice", - // ...additional payload - }, nil, nil) +func createServiceAccessToken() (string, error) { + validitySeconds := uint64(300) + useStaticSigningKey := false + + response, err := jwt.CreateJWT(map[string]interface{}{ + "iss": "https://auth.example.com", + "aud": "service-m2", + "sub": "service-m1", + "source": "microservice", + "token_type": "service_access", + "permissions": []string{"comments:write"}, + }, &validitySeconds, &useStaticSigningKey) if err != nil { - // handle error + return "", err + } + if response.OK == nil { + return "", errors.New("JWT creation failed") } - jwtString := jwtResponse.OK.Jwt - fmt.Println(jwtString) - // Send JWT as Authorization header to M2 + + return response.OK.Jwt, nil } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.jwt import asyncio from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult - -async def create_jwt(): - jwtResponse = await asyncio.create_jwt({ +response = await asyncio.create_jwt( + { + "iss": "https://auth.example.com", + "aud": "service-m2", + "sub": "service-m1", "source": "microservice", - # ... extra payload - }) - - if isinstance(jwtResponse, CreateJwtOkResult): - _ = jwtResponse.jwt - # Send JWT as Authorization header to M2 - else: - raise Exception("Unable to create JWT. Should never come here.") -``` - - - + "token_type": "service_access", + "permissions": ["comments:write"], + }, + validity_seconds=300, + use_static_signing_key=False, +) +if not isinstance(response, CreateJwtOkResult): + raise RuntimeError("JWT creation failed") +access_token = response.jwt +``` + + ```python from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult from supertokens_python.recipe.jwt.syncio import create_jwt -jwtResponse = create_jwt({ - "source": "microservice", - # ... extra payload -}) - -if isinstance(jwtResponse, CreateJwtOkResult): - jwtStr = jwtResponse.jwt - # Send JWT as Authorization header to M2 -else: - raise Exception("Unable to create JWT. Should never come here.") -``` - - - - - - - -:::note -By default, the lifetime of the JWT is a 100 years. You can pass a second argument to the `createJWT` function indicating a custom lifetime (in seconds) for the JWT. -::: - -:::note -By default, the JWT uses a static key for signing, not subject to the key rotation normally applied to access tokens. You can pass `false` as the third argument to the `createJWT` function to use the dynamic keys. -::: - - +response = create_jwt( + { + "iss": "https://auth.example.com", + "aud": "service-m2", + "sub": "service-m1", + "source": "microservice", + "token_type": "service_access", + "permissions": ["comments:write"], + }, + validity_seconds=300, + use_static_signing_key=False, +) +if not isinstance(response, CreateJwtOkResult): + raise RuntimeError("JWT creation failed") - +access_token = response.jwt +``` + + + + -You can send a `HTTP` request to the core as follows: +Prefer the backend SDK. It avoids manually constructing the Core request and keeps the API key out of command-line +arguments. If operational tooling must call the released Core API directly, provide the URL and headers through an +owner-readable curl config file (`0600`) populated by your secret tooling. Provide the request body over standard input: ```bash -curl --location --request POST '${connectionURI}/recipe/jwt' \ ---header 'rid: jwt' \ ---header 'api-key: ${APIKey}' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ +curl --config /run/secrets/supertokens-curl.conf --data-binary @- <<'JSON' +{ "payload": { + "iss": "https://auth.example.com", + "aud": "service-m2", + "sub": "service-m1", "source": "microservice", - ... + "token_type": "service_access", + "permissions": ["comments:write"] }, - "useStaticSigningKey": true, + "useStaticSigningKey": false, "algorithm": "RS256", - "jwksDomain": "${apiDomain}", - "validity": ${validityInSeconds} -}' -``` -- The value of `${connectionURI}` is the core's location -- `${APIKey}` is the API key to query the core. This is only needed if an API key exists on the core. -- The value of `${apiDomain}` is the domain on which the JWKs URLs are available from -- `${validityInSeconds}` is the lifetime of the JWT - -An example response is as follows: -```json -{ - "status": "OK", - "jwt": "eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA" -} - -``` - - - - - -### 2. Store the JWT - -Once you create the JWT, you can store it in a (globally accessible) variable and access it when you want to talk to a microservice. -Add the JWT as an `Authorization: Bearer` token like this: - -```bash -curl --location --request POST 'https://microservice_location/path' \ ---header 'Authorization: Bearer eyJraWQiOiI0YTE...rCFPcIRgzu_bChIIpFdA' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "request": "payload" -}' -``` - -### 3. Verify JWTs - - - -When a target microservice receives a JWT, it must first verify it before proceeding to serve the request. The process involves two steps: -- A standard verification of the JWT -- Checking the JWT claim to make sure that another microservice has queried it. - -#### Using JWKS endpoint - - - - -##### Get JWKS endpoint - -The JWKS endpoint is `^{appInfo.apiDomain}/^{appInfo.apiBasePath}/jwt/jwks.json`. Here the `apiDomain` and `apiBasePath` are values pointing to the server in which you have initialized SuperTokens using the backend SDK. - -##### Verify the JWT - -Some libraries let you provide a JWKS endpoint to verify a JWT. For example for NodeJS you can use `jsonwebtoken` and `jwks-rsa` together to achieve this. - -```ts -import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); - -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); + "jwksDomain": "https://auth.example.com", + "validity": 300 } - -let jwt = "..."; -JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { - let decodedJWT = decoded; - // Use JWT -}); +JSON ``` - - - -Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa) for a code reference of how use `PyJWK` to do JWT verification. The gist contains two files: -- `jwt_verification.py` (which you can copy/paste into your application). You have to modify the `JWKS_URI` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). This file is for `sync` python apps, and you can modify it to work with `async` apps as well. - - This file essentially exposes a function called `verify_jwt` which takes an input JWT string. - - This function takes care of caching public keys in memory + auto `refetching` if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts, and is just a security feature. -- `views.py`: This is an example `GET` API which extracts the JWT token from the authorization header in the request and calls the `verify_jwt` function from the other file. - - - - -Refer to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d) for a code reference of how use the Golang `jwt` lib to do session verification. The gist contains two files: -- `verifyToken.go` (which you can copy/paste into your application). You have to modify the `coreUrl` in this file to point to your SuperTokens core instance (replacing the `try.supertokens.com` part of the URL). - - This file essentially exposes a function called `GetJWKS` which returns a reference to the JWKS public keys useful for JWT verification. - - This function takes care of caching public keys in memory + auto `refetching` if the public keys have changed (which happens automatically every 24 hours with SuperTokens). This does not cause any user logouts, and is just a security feature. -- `main.go`: This is an example of how to verify a JWT using the golang JWT verification lib along with a helper function to get the JWKs keys. - - - - -#### Using public key string - - - - -Some JWT verification libraries require you to provide the JWT secret / public key for verification. You can obtain the JWT secret from SuperTokens in the following way: - -- First, query the `JWKS.json` endpoint: - ```bash - curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' - - { - "keys": [ - { - "kty": "RSA", - "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", - "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", - "e": "AQAB", - "alg": "RS256", - "use": "sig" - }, - { - "kty": "RSA", - "kid": "d-230...802340", - "n": "AMZruthvYz7...lx0rU8c=", - "e": "...", - "alg": "RS256", - "use": "sig" - } - ] - } - ``` - - :::important - The above shows an example output which returns two keys. There could be more keys returned based on the configured key rotation setting in the core. If you notice, each key's `kid` starts with a `s-..` or a `d-..`. The `s-..` key is a static key that never changes, whereas `d-...` keys are dynamic keys that keep changing. If you are `hardcoding` public keys somewhere, you always want to pick the `s-..` key. - ::: - -- Next, run the NodeJS script below to convert the above output to a `PEM` file format. - ```tsx - import jwkToPem from 'jwk-to-pem'; - - // This JWK is copied from the result of the above SuperTokens core request - let jwk = { - "kty": "RSA", - "kid": "s-2de612a5-a5ba-413e-9216-4c43e2e78c86", - "n": "AMZruthvYz7Ft-Dp0BC_SEEJaWK91s_YA-RR81iLJ6BTT6gJp0CcV4DfBynFU_59dRGOZyVQpAW6Drnc_6LyZpVWHROzqt-Fjh8TAqodayhPJVuZt25eQiYrqcaK_dnuHrm8qwUq-hko6q1o9NIIZWNfUBEVWmNhyAJFk5bi3pLwtKPYrUQzVLcTdDUe4SIltvvfpYHbVFnYtxkBVmqO68j7sI8ktmTXM_heals-W6WmozabDkC9_ITCeRat2f7A2l0t4QzO0ZCzZcJfhusF4X1niKgY6yYXpbX6is4HCfhYfdabcE52xYMNl-gw9XDjsIxfBMUDvOFRHWlx0rU8c=", - "e": "AQAB", - "alg": "RS256", - "use": "sig" - }; - - // @ts-ignore - let certString = jwkToPem(jwk); - ``` - - The above snippet would generate the following certificate string: - - ```text - -----BEGIN PUBLIC KEY----- - MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmu62G9jPsW34OnQEL9I - QQlpYr3Wz9gD5FHzWIsnoFNPqAmnQJxXgN8HKcVT/n11EY5nJVCkBboOudz/ovJm - ... (truncated for display) - XhfWeIqBjrJheltfqKzgcJ+Fh91ptwTnbFgw2X6DD1cOOwjF8ExQO84VEdaXHStT - xwIDAQAB - -----END PUBLIC KEY----- - ``` - -- You can use the generated PEM string in your code like shown below: - ```ts - import JsonWebToken from 'jsonwebtoken'; - - // Truncated for display - let certificate = "-----BEGIN PUBLIC KEY-----\nnMIIBIjANBgkqhki...\n-----END PUBLIC KEY-----"; - let jwt = "..."; // fetch the JWT from sAccessToken cookie or Authorization Bearer header - JsonWebToken.verify(jwt, certificate, function (err, decoded) { - let decodedJWT = decoded; - // Use JWT - }); - ``` - - - - -:::caution -Not applicable. Please use method 1 instead. -::: - - - - -:::caution -Not applicable. Please use method 1 instead. -::: - - - +Configure that file with the `/recipe/jwt` URL, `POST` method, `rid: jwt`, `Content-Type: application/json`, and +`api-key` header. Do not put the API key in command-line arguments, shell history, environment dumps, or generated logs. +Disable shell tracing such as `set -x` around secret handling, restrict access to the config file, and remove temporary +copies immediately after use. -#### Claim verification +Keep the token in memory only as long as needed. Send it as `Authorization: Bearer ` over TLS. Never log the +token or place it in a URL, source file, or long-lived configuration value. -The second step is to get the JWT payload and check that it has the `"source": "microservice"` claim: +## 3. Verify and authorize the JWT - - +The JWKS endpoint is: -```tsx -import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); - -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); -} - -let jwt = "..."; -JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { - // highlight-start - let decodedJWT = decoded; - if (decodedJWT === undefined || typeof decodedJWT === "string" || decodedJWT.source === undefined || decodedJWT.source !== "microservice") { - // return a 401 unauthorised error - } else { - // handle API request... - } - // highlight-end -}); +```text +/jwt/jwks.json ``` - - - -Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/ea31502923ec9a53136371f2b6317ffa), in `views.py`, between lines 20 and 28, the value of a certain claim in the decoded JWT payload undergoes a check. You can do something similar and check for the `source` claim whose value must be `microservice`. If it is, then all's good, else you can return a 401. - - - - -Referring once again to this [GitHub gist](https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d), in `main.go`, between lines 32 and 44, the value of a certain claim in the decoded JWT payload undergoes a check. You can do something similar and check for the `source` claim whose value must be `microservice`. If it is, then all's good, else you can return a 401. - - - - -#### M2M and frontend session verification for the same API -You may have a setup wherein the same API receives calls from the frontend as well as from other microservices. The frontend session works differently than m2m sessions, therefore both forms of token inputs must be accounted for. - -The approach here would be to first attempt frontend session verification, and if that fails, then attempt m2m JWT verification (using the above method). If both fail, then send back a `401` response. - -The [`getSession` function](https://supertokens.com/docs/session/common-customizations/sessions/session-verification-in-api/get-session) serves for frontend session verification. - - - - -```tsx -import express from "express"; -import Session from "supertokens-node/recipe/session"; -import JsonWebToken, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; -import jwksClient from 'jwks-rsa'; - -let app = express(); - - -var client = jwksClient({ - jwksUri: '^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json' -}); - -function getKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, function (err, key) { - var signingKey = key!.getPublicKey(); - callback(err, signingKey); - }); -} - - -app.post("/like-comment", async (req, res, next) => { - // highlight-start - try { - let session = await Session.getSession(req, res, { sessionRequired: false }) - - if (session !== undefined) { - // API call from the frontend and session verification is successful.. - let userId = session.getUserId(); - } else { - // maybe this API is called from a microservice, so we attempt JWT verification as - // shown above. - let jwt = req.headers["authorization"]; - jwt = jwt === undefined ? undefined : jwt.split('Bearer ')[1]; - if (jwt === undefined) { - // return a 401 unauthorised error... - } else { - JsonWebToken.verify(jwt, getKey, {}, function (err, decoded) { - let decodedJWT = decoded; - // microservices auth is successful.. - }); - } - } - } catch (err) { - next(err); - } - // highlight-end -}); -``` - -- Notice that the `sessionRequired: false` option appears when calling `getSession`. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, the `getSession` function returns `undefined`. It's important to note that if the session does exist, but the access token has expired, the `getSession` function throws a try refresh token error, sending a 401 to the frontend. This triggers a session refresh flow as expected. -- If the `getSession` function returns `undefined`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. -- If that fails too, send back a `401` response. - - - - -```python -from typing import Optional, cast - -from django.http import HttpRequest - -from supertokens_python.recipe.session.asyncio import get_session - - -async def like_comment(request: HttpRequest): - session = await get_session(request, session_required=False) - user_id = "" - if session is not None: - user_id = session.get_user_id() - else: - jwt: Optional[str] = cast(Optional[str], request.headers.get("Authorization")) # type: ignore - if jwt is None: - # return a 401 unauthorised error... - pass - else: - jwt = jwt.split("Bearer ")[1] - # JWT verification (see previous step) - pass - - print(user_id) # TODO -``` - -- Notice that the `sessionRequired: false` option appears when calling `getSession`. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, the `getSession` function returns `None`. It's important to note that if the session does exist, but the access token has expired, the `getSession` function throws a try refresh token error, sending a 401 to the frontend. This triggers a session refresh flow as expected. -- If the `getSession` function returns `None`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. -- If that fails too, send back a `401` response. - - - - -```go -import ( - "fmt" - "net/http" - "strings" - - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func likeCommentAPI(w http.ResponseWriter, r *http.Request) { - sessionRequired := false - sessionContainer, err := session.GetSession(r, w, &sessmodels.VerifySessionOptions{ - SessionRequired: &sessionRequired, - }) - - if err != nil { - err = supertokens.ErrorHandler(err, r, w) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - return - } - - if sessionContainer != nil { - userID := sessionContainer.GetUserID() - - // TODO: API logic... - fmt.Println(userID) - } else { - // Check for JWT in the Authorization header - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - // Return 401 Unauthorized error - w.WriteHeader(http.StatusUnauthorized) - return - } - - // Split the Authorization header to get the JWT - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { - // Return 401 Unauthorized error - w.WriteHeader(http.StatusUnauthorized) - return - } - - jwt := parts[1] - - fmt.Println(jwt) - - // verify JWT based on code snippet here: https://gist.github.com/rishabhpoddar/8c26ed237add1a5b86481e72032abf8d - } -} -``` - -- Notice that the `sessionRequired: false` option appears when calling `getSession`. This is because in case the input tokens are from another microservice, then instead of throwing an unauthorised error, the `getSession` function returns `nil`. It's important to note that if the session does exist, but the access token has expired, the `getSession` function returns an error, and the `supertokens.ErrorHandler` sends a 401 to the frontend. This triggers a session refresh flow as expected. -- If the `getSession` function returns `nil`, it means that the session is not from the frontend and a microservice auth verification can occur using the JWT verification method shown previously in this page. -- If that fails too, send back a `401` response. - - - +With the default API base path, this is `https://auth.example.com/auth/jwt/jwks.json`. Configure a maintained JWT +library to fetch and cache this JWKS, honor its cache behavior, and refetch when it encounters an unknown `kid`. Dynamic +keys rotate every week by default. Static keys may also appear in JWKS, but they do not rotate and must not be selected +or hardcoded for this flow. + +Do not trust decoded data until the verification library reports success. Verification must: + +1. Allow only `RS256`; reject an unexpected or missing `alg` or `kid`. +2. Verify the signature with the JWKS key selected by `kid`. +3. Reject every library error before reading claims. This includes an invalid signature, expired token, malformed token, + unknown key, and issuer or audience mismatch. +4. Require `exp` and reject expired tokens. Do not disable expiry verification or add an unbounded clock tolerance. +5. Require exact expected values for `iss`, `aud`, `source`, and `token_type`. +6. Require an approved `sub` service identity and every permission needed by the endpoint. + +For the example above, `M2` must require: + +| Claim | Required value | +| --- | --- | +| `iss` | `https://auth.example.com` | +| `aud` | `service-m2` | +| `sub` | An approved calling service, such as `service-m1` | +| `source` | `microservice` | +| `token_type` | `service_access` | +| `permissions` | Includes the endpoint's required permission | +| `exp` | Present and in the future | + +Return `401 Unauthorized` when authentication fails. Return `403 Forbidden` when the token is valid but its service or +permissions do not authorize the operation. Do not reveal signature, key, or claim-validation details to the caller. + +### Idempotent writes and replay + +A valid bearer token can be replayed until it expires. For non-idempotent writes, require a caller-generated +`Idempotency-Key` scoped to the authenticated service and operation. Atomically reserve the key in shared durable storage +before the side effect, and return the stored result for an identical retry. Reject reuse with different request data, +and retain the record for a bounded period covering the retry window. + +If a token must be accepted only once, add a server-generated, unpredictable, unique `jti` claim when creating it. Before +the side effect, atomically insert `(iss, sub, jti)` into replay storage shared by every service instance; reject the +request if it already exists. Keep the entry until at least `exp` plus the permitted clock skew. Couple replay reservation +and the write transaction, or use a transactional outbox, so a crash cannot consume the token without a defined result. +An `Idempotency-Key` or `jti` is not a substitute for signature, claim, identity, and permission verification. + +### APIs that accept frontend sessions and service tokens + +Prefer separate endpoints or an explicit authentication policy for frontend sessions and service tokens. If one endpoint +must accept both, verify each credential only with its intended verifier and apply a separate authorization policy. Never +fall back to trusting decoded JWT claims after either verifier returns an error. A malformed, expired, or invalid token +must not be downgraded into another authentication path. + +Use the backend SDK's `getSession` function for frontend session verification. Use the bounded JWKS procedure above for +legacy service tokens. Accept the request only after one verifier succeeds and the corresponding identity and permission +checks pass. + +## Compromise response + +- **Core API key compromised:** revoke and replace it, stop token issuance while investigating, and wait at least the + maximum token lifetime before considering previously minted tokens expired. Review issuance and service logs. +- **Dynamic signing key compromised:** rotate the signing material, prevent further issuance, and reject affected keys. + Network restrictions can reduce exposure but do not make forged tokens safe. +- **Bearer token compromised:** revoke or disable the caller where possible and let the short expiry bound exposure. If + immediate revocation is required, use an introspected or stateful design rather than this self-contained legacy flow. diff --git a/docs/authentication/m2m/meta.ts b/docs/authentication/m2m/meta.ts new file mode 100644 index 0000000000..686032b979 --- /dev/null +++ b/docs/authentication/m2m/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Machine to Machine", + icon: "bot", + order: 70, +}); diff --git a/docs/authentication/meta.ts b/docs/authentication/meta.ts new file mode 100644 index 0000000000..79c3f247b3 --- /dev/null +++ b/docs/authentication/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Authentication", + icon: "key-round", + order: 40, + collapsed: true, +}); diff --git a/docs/authentication/overview.mdx b/docs/authentication/overview.mdx index 64773e0707..d4694e2f10 100644 --- a/docs/authentication/overview.mdx +++ b/docs/authentication/overview.mdx @@ -1,125 +1,57 @@ --- title: Overview -sidebar_position: 1 -pagination_next: null -pagination_prev: null -hide_table_of_contents: true -description: >- - Explore various user authentication methods with SuperTokens, including - email/password, social login, and more. -page_type: overview -category: authentication +description: Explore various user authentication methods with SuperTokens, including email/password, social login, and more. +sidebar: + order: 10 --- - -# Overview - Discover all the ways in which you can authenticate your users with **SuperTokens**. ## Authentication Methods - - - - Email/Password - - - Basic authentication using email and password. - - - - - Passwordless - - - Authentication through magic links or one-time codes. - - - - - - Social Login - - - Login flow that uses third-party providers for authentication. - - - - - Passkeys - - - Passwordless authentication using biometrics, security keys, or device-based credentials. - - - - - - Enterprise Login - - - Instructions on how to configure your application to support multiple tenants and enterprise authentication methods. - - - - - Unified Login - - - Details on how to create a common authentication experience for all your products. - - - - - - Machine to Machine Authentication - - - Guides on authenticating microservices using SuperTokens. - - - + + +Basic authentication using email and password. + + +Authentication through magic links or one-time codes. + + +Login flow that uses third-party providers for authentication. + + +Passwordless authentication using biometrics, security keys, or device-based credentials. + + +Instructions on how to configure your application to support multiple tenants and enterprise authentication methods. + + +Details on how to create a common authentication experience for all your products. + + +Guides on authenticating microservices using SuperTokens. + + ## Additional Resources For information on other features exposed by SuperTokens, please refer to the following resources: - - - - Multi-Factor Authentication - - - Set up additional verification layers in your sign-in process. - - - - - Attack Protection Suite - - - Enable additional security features that shield your app. - - - - - - Self Hosting - - - Deploy SuperTokens in your own infrastructure. - - - - - - Migration Guide - - - Learn how to migrate from an existing authentication provider. - - - - + + +Set up additional verification layers in your sign-in process. + + +Enable additional security features that shield your app. + + +Deploy SuperTokens in your own infrastructure. + + + +Learn how to migrate from an existing authentication provider. + + + diff --git a/docs/authentication/passkeys/_category_.json b/docs/authentication/passkeys/_category_.json deleted file mode 100644 index 71a1f5055d..0000000000 --- a/docs/authentication/passkeys/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Passkeys", - "position": 7 -} diff --git a/docs/authentication/passkeys/customization.mdx b/docs/authentication/passkeys/customization.mdx index fe0a2fc0c7..cc72940afc 100644 --- a/docs/authentication/passkeys/customization.mdx +++ b/docs/authentication/passkeys/customization.mdx @@ -1,15 +1,9 @@ --- title: Customization -hide_title: true -sidebar_position: 4 -page_type: guide -recipe: webauthn -category: passkeys +sidebar: + order: 4 --- - -# Customization - ## Overview Like the other **SuperTokens** authentication recipes, you can customize the `WebAuthn` flow through different configuration options and overrides. @@ -19,73 +13,90 @@ The following page describes the options that you can change and the different s ## Backend recipe configuration - - - - + + ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ - framework: "express", - supertokens: { - // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. - connectionURI: "https://try.supertokens.com", - // apiKey: , - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "/auth", - websiteBasePath: "/auth" - }, - recipeList: [ - WebAuthn.init({ - getOrigin: () => { - return "https://example.com"; - }, - getRelyingPartyId: () => { - return "example.com"; - }, - getRelyingPartyName: () => { - return "example"; - }, - }), - Session.init() // initializes session features - ] + framework: "express", + supertokens: { + // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. + connectionURI: "https://try.supertokens.com", + // apiKey: , + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + WebAuthn.init({ + getOrigin: async () => { + return "https://example.com"; + }, + getRelyingPartyId: async () => { + return "example.com"; + }, + getRelyingPartyName: async () => { + return "example"; + }, + }), + Session.init(), // initializes session features + ], }); ``` + + +```go +import ( + "net/http" + + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/webauthn" + "github.com/supertokens/supertokens-golang/recipe/webauthn/webauthnmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) -The backend recipe accepts the following properties during initialization: - -| Option | Description | Default | -|--------|-------------|---------| -| `getRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | The `apiDomain` value that you have set in `appConfig` | -| `getRelyingPartyName` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `appName` value that you have set in `appConfig` | -| `getOrigin` | Configures the origin URL that WebAuthn credentials bind to. This should match your application's domain and protocol. | Origin of the request | -| `emailDelivery` | Configures how the system builds and sends verification emails to users. Read the [email delivery page](/docs/platform-configuration/email-delivery) for more information. | Default email service | -| `validateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | - -All the properties are optional. - - - - - -:::caution - -At the moment there is no support for using passkeys authentication in the Go SDK. - -::: - - - - - +func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ConnectionURI: "https://try.supertokens.com"}, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + webauthn.Init(&webauthnmodels.TypeInput{ + GetOrigin: func(tenantID string, req *http.Request, userContext supertokens.UserContext) (string, error) { + return "https://example.com", nil + }, + GetRelyingPartyId: func(tenantID string, req *http.Request, userContext supertokens.UserContext) (string, error) { + return "example.com", nil + }, + GetRelyingPartyName: func(tenantID string, userContext supertokens.UserContext) (string, error) { + return "example", nil + }, + }), + session.Init(nil), + }, + }) + if err != nil { + panic(err) + } +} +``` + + ```python from typing import Optional @@ -131,116 +142,149 @@ init( ] ) ``` + + + + The backend recipe accepts the following properties during initialization: | Option | Description | Default | |--------|-------------|---------| -| `get_relying_party_id` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | The `api_domain` value that you have set in `app_config` | +| `getRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | Hostname of `appInfo.apiDomain` | +| `getRelyingPartyName` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `appName` value that you have set in `appConfig` | +| `getOrigin` | Configures the frontend origin that WebAuthn credentials bind to. | `appInfo.getOrigin(...)`, normally the configured website origin | +| `emailDelivery` | Configures how the system builds and sends account-recovery emails. Read the [email delivery page](/platform-configuration/email-delivery) for more information. | Default email service | +| `validateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | + +All the properties are optional. + + +The backend recipe accepts the following properties during initialization: + +| Option | Description | Default | +|--------|-------------|---------| +| `get_relying_party_id` | Sets the domain name associated with the WebAuthn credentials. This helps ensure that only your domain uses the credentials. | Hostname of `app_info.api_domain` | | `get_relying_party_name` | Sets a human-readable name for your application. The name appears to users during the WebAuthn registration process. | The `app_name` value that you have set in `app_config` | -| `get_origin` | Configures the origin URL that WebAuthn credentials bind to. This should match your application's domain and protocol. | Origin of the request | -| `email_delivery` | Configures how the system builds and sends verification emails to users. Read the [email delivery page](/docs/platform-configuration/email-delivery) for more information. | Default email service | +| `get_origin` | Configures the frontend origin that WebAuthn credentials bind to. | `app_info.get_origin(...)`, normally the configured website origin | +| `email_delivery` | Configures how the system builds and sends account-recovery emails. Read the [email delivery page](/platform-configuration/email-delivery) for more information. | Default email service | | `validate_email_address` | Adds custom validation logic for email addresses. | Basic email format validation | All the properties are optional. + + +The backend recipe accepts the following optional properties in `webauthnmodels.TypeInput`: - +| Option | Description | Default | +|--------|-------------|---------| +| `GetRelyingPartyId` | Sets the domain name associated with the WebAuthn credentials. | Hostname of `AppInfo.APIDomain` | +| `GetRelyingPartyName` | Sets a human-readable name for your application. | `AppInfo.AppName` | +| `GetOrigin` | Configures the frontend origin that WebAuthn credentials bind to. | `AppInfo.GetOrigin(...)`, normally the configured website origin | +| `EmailDelivery` | Configures how the system builds and sends account-recovery emails. | Default email service | +| `ValidateEmailAddress` | Adds custom validation logic for email addresses. | Basic email format validation | + + - +The RP ID must equal the frontend origin's host or be a registrable domain suffix of it. It must not include a scheme, port, or path. The origin must include the exact scheme and host, plus the port when it is non-default. For example, RP ID `example.com` is valid for origin `https://login.example.com`, but `api.example.net` is not. If your API and website use unrelated hosts, the default RP ID derived from the API domain is invalid for the website origin; configure both values explicitly. --- ## Credential generation The client generates the credentials based on the options provided by the backend SDK. -The frontend SDK uses the [`navigator.credentials.created`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create) function to resolve this. +The frontend SDK uses [`navigator.credentials.create()`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create) to start the registration ceremony. To change the options used to generate credentials, you need to override the `registerOptions` function. - - - - + + ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ - framework: "express", - supertokens: { - // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. - connectionURI: "https://try.supertokens.com", - // apiKey: , - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "/auth", - websiteBasePath: "/auth" - }, - recipeList: [ - WebAuthn.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - registerOptions: (input) => { - return originalImplementation.registerOptions({ - ...input, - attestation: "direct", - residentKey: "required", - timeout: 10 * 1000, - userVerification: "required", - displayName: "John Doe", - supportedAlgorithms: [-257], - relyingPartyId: 'example.com', - relyingPartyName: 'example', - origin: 'https://example.com', - }); - }, - }; - }, + framework: "express", + supertokens: { + // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. + connectionURI: "https://try.supertokens.com", + // apiKey: , + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + WebAuthn.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + registerOptions: (input) => { + return originalImplementation.registerOptions({ + ...input, + attestation: "direct", + residentKey: "required", + timeout: 10 * 1000, + userVerification: "required", + userPresence: true, + displayName: "John Doe", + supportedAlgorithmIds: [-257], + relyingPartyId: "example.com", + relyingPartyName: "example", + origin: "https://example.com", + }); }, - }), - Session.init() // initializes session features - ] + }; + }, + }, + }), + Session.init(), // initializes session features + ], }); ``` - -
- -#### Input properties - -| Name | Type | Description | Default Value | -|----------|----------|-------------|---------------| -| `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration which defaults to the `apiDomain` | -| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration which defaults to the `apiName` | -| `origin` | `string` | The origin URL where the credential is generated. | Uses `getOrigin` from the recipe configuration which defaults to the origin of the request | -| `timeout` | `number` | The time in milliseconds that the user has to complete the credential generation process. | `6000` | -| `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The amount of information about the authenticator that gets included in the attestation statement. This controls what authenticators support. | `none` | -| `supportedAlgorithms` | `number[]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | -| `residentKey` | `"discouraged" \| "preferred" \| "required"` | Whether the credential gest stored on the authenticator device. | `required` | -| `userVerification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | -| `displayName` | `string` | The display name of the user. | The user's `email` property | - - -
- - - -:::caution - -At the moment there is no support for using passkeys authentication in the Go SDK. - -::: - - - - - + + +```go check=false reason="Override excerpt; pass config to webauthn.Init in your recipe list." +config := &webauthnmodels.TypeInput{ + Override: &webauthnmodels.OverrideStruct{ + Functions: func(original webauthnmodels.RecipeInterface) webauthnmodels.RecipeInterface { + originalRegisterOptions := *original.RegisterOptions + registerOptions := func( + email, recoverAccountToken, displayName *string, + relyingPartyID, relyingPartyName, origin string, + timeout *int, attestation *webauthnmodels.Attestation, + residentKey *webauthnmodels.ResidentKey, + userVerification *webauthnmodels.UserVerification, + userPresence *bool, + supportedAlgorithmIDs []webauthnmodels.COSEAlgorithmIdentifier, + tenantID string, userContext supertokens.UserContext, + ) (webauthnmodels.RegisterOptionsResponse, error) { + customTimeout := 10 * 1000 + customAttestation := webauthnmodels.AttestationDirect + customResidentKey := webauthnmodels.ResidentKeyRequired + customUserVerification := webauthnmodels.UserVerificationRequired + customUserPresence := true + return originalRegisterOptions( + email, recoverAccountToken, displayName, + "example.com", "example", "https://example.com", + &customTimeout, &customAttestation, &customResidentKey, + &customUserVerification, &customUserPresence, + []webauthnmodels.COSEAlgorithmIdentifier{-257}, tenantID, userContext, + ) + } + original.RegisterOptions = ®isterOptions + return original + }, + }, +} +``` + + ```python from typing import List, Optional, cast @@ -248,14 +292,17 @@ from typing_extensions import Unpack from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn -from supertokens_python.recipe.webauthn import RecipeInterface, WebauthnConfig +from supertokens_python.recipe.webauthn import ( + RecipeInterface, + WebauthnConfig, + WebauthnOverrideConfig, +) from supertokens_python.recipe.webauthn.interfaces.recipe import ( Attestation, RegisterOptionsKwargsInput, ResidentKey, UserVerification, ) -from supertokens_python.recipe.webauthn.types.config import OverrideConfig from supertokens_python.types.base import UserContext @@ -285,6 +332,7 @@ def override_webauthn_functions(original_implementation: RecipeInterface): user_verification="required", user_presence=True, attestation="direct", + supported_algorithm_ids=[-257], timeout=10 * 1000, tenant_id=tenant_id, user_context=user_context, @@ -314,125 +362,173 @@ init( recipe_list=[ webauthn.init( config=WebauthnConfig( - override=OverrideConfig(functions=override_webauthn_functions) + override=WebauthnOverrideConfig(functions=override_webauthn_functions) ) ), session.init(), # initializes session features ], ) ``` + + + + +
+ +#### Input properties + +| Name | Type | Description | Default Value | +|----------|----------|-------------|---------------| +| `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration, which defaults to the hostname of `appInfo.apiDomain` | +| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration, which defaults to `appName` | +| `origin` | `string` | The frontend origin where the credential is created. | Uses `getOrigin` from the recipe configuration, which normally defaults to the configured website origin | +| `timeout` | `number` | The time in milliseconds that the user has to complete the credential generation process. | `60000` | +| `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The attestation conveyance preference requested from the authenticator. | `none` | +| `supportedAlgorithmIds` | `number[]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | +| `residentKey` | `"discouraged" \| "preferred" \| "required"` | Whether the authenticator creates a discoverable credential. A discoverable credential may be synced or device-bound. | `required` | +| `userVerification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | +| `userPresence` | `boolean` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `true` | +| `displayName` | `string` | The display name of the user. | The user's `email` property | +
+
#### Input properties | Name | Type | Description | Default Value | |----------|----------|-------------|---------------| -| `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration which defaults to the `api_domain` | +| `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration, which defaults to the hostname of `app_info.api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | -| `origin` | `str` | The origin URL where the credential is generated. | Uses `get_origin` from the recipe configuration which defaults to the origin of the request | -| `timeout` | `int` | The time in milliseconds that the user has to complete the credential generation process. | `6000` | -| `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The amount of information about the authenticator that gets included in the attestation statement. This controls what authenticators support. | `none` | -| `supported_algorithms` | `List[int]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | -| `resident_key` | `"discouraged" \| "preferred" \| "required"` | Whether the credential gest stored on the authenticator device. | `required` | +| `origin` | `str` | The frontend origin where the credential is created. | Uses `get_origin` from the recipe configuration, which normally defaults to the configured website origin | +| `timeout` | `int` | The time in milliseconds that the user has to complete the credential generation process. | `60000` | +| `attestation` | `"none" \| "indirect" \| "direct" \| "enterprise"` | The attestation conveyance preference requested from the authenticator. | `none` | +| `supported_algorithm_ids` | `List[int]` | The cryptographic algorithms that can generate credentials. Different authenticators support different algorithms. | `[-8, -7, -257]` | +| `resident_key` | `"discouraged" \| "preferred" \| "required"` | Whether the authenticator creates a discoverable credential. A discoverable credential may be synced or device-bound. | `required` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | Whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | +| `user_presence` | `bool` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `True` | | `display_name` | `str` | The display name of the user. | The user's `email` property | +
+ +
+#### Input properties -
- -
+| Name | Type | Description | Default Value | +|----------|----------|-------------|---------------| +| `relyingPartyId` | `string` | The domain name used to validate the credential. | Uses `GetRelyingPartyId`, which defaults to the hostname of `AppInfo.APIDomain` | +| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `GetRelyingPartyName`, which defaults to `AppInfo.AppName` | +| `origin` | `string` | The frontend origin where the credential is created. | Uses `GetOrigin`, which normally defaults to the configured website origin | +| `timeout` | `*int` | The time in milliseconds available to complete credential creation. | `60000` | +| `attestation` | `*webauthnmodels.Attestation` | The attestation conveyance preference requested from the authenticator. | `AttestationNone` | +| `supportedAlgorithmIds` | `[]webauthnmodels.COSEAlgorithmIdentifier` | The allowed credential algorithms. | `[-8, -7, -257]` | +| `residentKey` | `*webauthnmodels.ResidentKey` | Whether the authenticator creates a discoverable credential. | `ResidentKeyRequired` | +| `userVerification` | `*webauthnmodels.UserVerification` | Whether user verification, such as a PIN or biometrics, is necessary. | `UserVerificationPreferred` | +| `userPresence` | `*bool` | Whether the ceremony requires evidence of user interaction. | `true` | +| `displayName` | `*string` | The display name of the user. | The user's email | + + + +Keep the default `attestation: "none"` unless your relying party has a specific attestation policy. `"direct"` can expose identifying authenticator information and still requires you to validate the attestation statement and its certificate chain against trust anchors you maintain. Requesting direct attestation does not by itself make an authenticator trusted. --- ## Credential validation -When a user attempts to login, the authenticator uses their credential to sign a challenge on the client. -The frontend SDK uses the [`navigator.credentials.get`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get) function to resolve this. +When a user attempts to sign in, the authenticator uses their credential to sign an assertion on the client. +The frontend SDK uses [`navigator.credentials.get()`](https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get) to start the authentication ceremony. -The server generates the options for signing the challenge through the backend SDK, and then sends them to the client. +The server generates the options for signing the challenge through the backend SDK, and then sends them to the client. To change those, you need to override the `signInOptions` function. - - - - + + ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthn from "supertokens-node/recipe/webauthn"; supertokens.init({ - framework: "express", - supertokens: { - // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. - connectionURI: "https://try.supertokens.com", - // apiKey: , - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "/auth", - websiteBasePath: "/auth" - }, - recipeList: [ - WebAuthn.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signInOptions: (input) => { - return originalImplementation.signInOptions({ - ...input, - timeout: 10 * 1000, - userVerification: "required", - relyingPartyId: 'example.com', - origin: 'https://example.com', - }); - }, - }; - }, + framework: "express", + supertokens: { + // https://try.supertokens.com is for demo purposes. Replace this with the address of your core instance (sign up on supertokens.com), or self host a core. + connectionURI: "https://try.supertokens.com", + // apiKey: , + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + WebAuthn.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signInOptions: (input) => { + return originalImplementation.signInOptions({ + ...input, + timeout: 10 * 1000, + userVerification: "required", + userPresence: true, + relyingPartyId: "example.com", + origin: "https://example.com", + }); }, - }), - Session.init() // initializes session features - ] + }; + }, + }, + }), + Session.init(), // initializes session features + ], }); ``` - -#### Input properties - -| Name | Type | Description | Default | -|----------|----------|-------------|---------| -| `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration which defaults to the `apiDomain` | -| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration which defaults to the `apiName` | -| `origin` | `string` | The origin URL where the credential is generated. | Uses `getOrigin` from the recipe configuration which defaults to the origin of the request | -| `timeout` | `number` | The time in milliseconds that the user has to complete the credential validation process. | `6000` | -| `userVerification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | - - - - - -:::caution - -At the moment there is no support for using passkeys authentication in the Go SDK. - -::: - - - - - + + +```go check=false reason="Override excerpt; pass config to webauthn.Init in your recipe list." +config := &webauthnmodels.TypeInput{ + Override: &webauthnmodels.OverrideStruct{ + Functions: func(original webauthnmodels.RecipeInterface) webauthnmodels.RecipeInterface { + originalSignInOptions := *original.SignInOptions + signInOptions := func( + relyingPartyID, relyingPartyName, origin string, + timeout *int, + userVerification *webauthnmodels.UserVerification, + userPresence *bool, + tenantID string, userContext supertokens.UserContext, + ) (webauthnmodels.SignInOptionsResponse, error) { + customTimeout := 10 * 1000 + customUserVerification := webauthnmodels.UserVerificationRequired + customUserPresence := true + return originalSignInOptions( + "example.com", "example", "https://example.com", + &customTimeout, &customUserVerification, &customUserPresence, + tenantID, userContext, + ) + } + original.SignInOptions = &signInOptions + return original + }, + }, +} +``` + + ```python from typing import Any from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn -from supertokens_python.recipe.webauthn import RecipeInterface, WebauthnConfig -from supertokens_python.recipe.webauthn.types.config import OverrideConfig +from supertokens_python.recipe.webauthn import ( + RecipeInterface, + WebauthnConfig, + WebauthnOverrideConfig, +) from supertokens_python.types.base import UserContext @@ -450,6 +546,7 @@ def override_webauthn_functions(original_implementation: RecipeInterface): user_context=user_context, timeout=10 * 1000, user_verification="required", + user_presence=True, relying_party_id='example.com', relying_party_name='Example', origin='https://example.com', @@ -475,7 +572,7 @@ init( recipe_list=[ webauthn.init( config=WebauthnConfig( - override=OverrideConfig( + override=WebauthnOverrideConfig( functions=override_webauthn_functions ) ) @@ -484,19 +581,44 @@ init( ] ) ``` + + + + + +#### Input properties +| Name | Type | Description | Default | +|----------|----------|-------------|---------| +| `relyingPartyId` | `string` | The domain name of your application that the system uses for validating the credential. | Uses `getRelyingPartyId` from the recipe configuration, which defaults to the hostname of `appInfo.apiDomain` | +| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `getRelyingPartyName` from the recipe configuration, which defaults to `appName` | +| `origin` | `string` | The expected frontend origin for the authentication response. | Uses `getOrigin` from the recipe configuration, which normally defaults to the configured website origin | +| `timeout` | `number` | The time in milliseconds that the user has to complete the credential validation process. | `60000` | +| `userVerification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | +| `userPresence` | `boolean` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `true` | + + #### Input properties | Name | Type | Description | Default | |----------|----------|-------------|---------| -| `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration which defaults to the `api_domain` | +| `relying_party_id` | `str` | The domain name of your application that the system uses for validating the credential. | Uses `get_relying_party_id` from the recipe configuration, which defaults to the hostname of `app_info.api_domain` | | `relying_party_name` | `str` | The human-readable name of your application. | Uses `get_relying_party_name` from the recipe configuration which defaults to the `app_name` | -| `origin` | `str` | The origin URL where the credential is generated. | Uses `get_origin` from the recipe configuration which defaults to the origin of the request | -| `timeout` | `int` | The time in milliseconds that the user has to complete the credential validation process. | `6000` | +| `origin` | `str` | The expected frontend origin for the authentication response. | Uses `get_origin` from the recipe configuration, which normally defaults to the configured website origin | +| `timeout` | `int` | The time in milliseconds that the user has to complete the credential validation process. | `60000` | | `user_verification` | `"discouraged" \| "preferred" \| "required"` | The parameter controls whether user verification (like `PIN` or biometrics) is necessary. | `preferred` | +| `user_presence` | `bool` | Whether the ceremony requires evidence of user interaction. This is separate from user verification. | `True` | + + +#### Input properties - - - - - +| Name | Type | Description | Default | +|----------|----------|-------------|---------| +| `relyingPartyId` | `string` | The domain name used to validate the credential. | Uses `GetRelyingPartyId`, which defaults to the hostname of `AppInfo.APIDomain` | +| `relyingPartyName` | `string` | The human-readable name of your application. | Uses `GetRelyingPartyName`, which defaults to `AppInfo.AppName` | +| `origin` | `string` | The expected frontend origin for the authentication response. | Uses `GetOrigin`, which normally defaults to the configured website origin | +| `timeout` | `*int` | The time in milliseconds available to complete authentication. | `60000` | +| `userVerification` | `*webauthnmodels.UserVerification` | Whether user verification, such as a PIN or biometrics, is necessary. | `UserVerificationPreferred` | +| `userPresence` | `*bool` | Whether the ceremony requires evidence of user interaction. | `true` | + + diff --git a/docs/authentication/passkeys/important-concepts.mdx b/docs/authentication/passkeys/important-concepts.mdx index f3d4dd7034..5813cc1fe2 100644 --- a/docs/authentication/passkeys/important-concepts.mdx +++ b/docs/authentication/passkeys/important-concepts.mdx @@ -1,14 +1,9 @@ --- title: Important concepts -hide_title: true -sidebar_position: 2 -page_type: guide -recipe: webauthn -category: passkeys +sidebar: + order: 2 --- -# Important concepts - ## Overview Use this page to get a high-level overview of the key concepts involved in the WebAuthn documentation. @@ -27,64 +22,53 @@ For a more detailed explanation of WebAuthn, you can refer to the [actual specif ### Passkeys -A **passkey** is a type of credential that implements the WebAuthn standard. -It uses cryptographic keys that users can share across multiple devices. -This makes it convenient and recoverable in case of device loss. - -Aside from the ease of use, passkeys are integrated into operating systems and browsers, and, support a wide range of devices. +A **passkey** is a discoverable WebAuthn credential. A passkey may be synced across a user's devices by a credential provider, or it may remain bound to one authenticator. +Synced passkeys can make recovery from device loss easier; device-bound passkeys require another recovery path. +Passkeys are integrated into operating systems and browsers and support a wide range of devices. ### Additional terms - - -## Credentials + + +A **WebAuthn credential** is an RP-scoped public-key credential created by an authenticator. Credentials can be discoverable or non-discoverable. A passkey is a discoverable credential and can be either synced or device-bound. -The technical term for the cryptographic key pairs used in **WebAuthn**. -They represent the raw cryptographic material and, unlike **passkeys**, are device-specific and not synced. -Additionally, sometimes, multiple credentials can be part of a single passkey. - -:::info Note -This documentation uses **passkey** and **credential** interchangeably, even though subtle technical differences exist. +:::info[Note] +This documentation uses **credential** for the WebAuthn object and **passkey** only when referring to a discoverable credential. ::: - -## Authenticator - + + A device or software that implements the **WebAuthn** authentication. This can be: - **Platform Authenticator**: Built-in biometric sensors like TouchID, FaceID, or Windows Hello. - **Roaming Authenticator**: External security devices like YubiKeys or Google Titan keys. - -## Registration - + + The process where a user registers their **authenticator** with your application. During this process: - 1. The server generates a challenge for the **authenticator** to sign. - 2. The **authenticator** creates a new credential. - 3. The system saves the public key and any additional metadata for future authentication. - -## Authentication - + 1. The server generates registration options containing a challenge and RP information. + 2. The browser calls `navigator.credentials.create()`, and the authenticator creates a credential and returns an attestation response bound to the challenge. + 3. The server validates the response and stores the public key and required metadata for future authentication. + + The process where a user proves their identity, using their **authenticator**, by responding to a **server challenge**. Using their private key, they sign the **challenge** and then send the result to the server. The server then verifies the signature with the stored public key.
- -## Attestation - -**Attestation** represents information about the **authenticator device** itself. -You can use it to verify the authenticity and the security level of the **authenticator**. +
+ +**Attestation** can provide evidence about an authenticator's provenance and capabilities. A relying party can evaluate that evidence against its policy and trusted attestation roots; attestation alone does not establish a device's general trustworthiness or security level.
+
+ +**User presence** confirms that a person interacted with the authenticator, for example by touching a security key. It does not identify or verify that person. -## User verification - -**User verification** is the method used to verify the user's presence. This can be: +**User verification** confirms that the person is authorized to use the authenticator. This can use: - Biometric verification (fingerprint, face scan). - `PIN` entry. - - Physical button press on a security key. - +
## Authentication flows @@ -94,47 +78,72 @@ This section explains how each component communicates during different authentic ### Login - -## The frontend SDK requests registration options from the backend. + The options are then returned based on the response from the **SuperTokens** core service. -## The **authenticator** uses the response to sign a challenge with your **passkey**. -## The result of the **authenticator** operation gets validated by the **SuperTokens** core service. -## The authentication UI updates, based on the result of the validation process. + + + + + + + + + -![Sign in form UI for passkeys login](/img/webauthn-signin.png) +Sign in form UI for passkeys login ### Sign up + -## The user enters their email address in frontend authentication UI -## The frontend SDK uses the email to request **registration** options from the backend. + + The options are then returned based on the response from the **SuperTokens** core service. -## The **authenticator** uses the response to sign a **challenge** with your **passkey**. -## The result of the **authenticator** operation gets validated on by the **SuperTokens** core service. -## The authentication UI updates, based on the result of the validation process. + + + + + + + + + -![Passkeys sign up flow](/img/webauthn-signup.png) +Passkeys sign up flow ### Account recovery -Account recovery should use an email. -In it, the user receives a link that directs them to a page where they can register a new credential. +SuperTokens account recovery uses an email containing a link to a page where the user can register a new credential. + -## The frontend initiates the recovery flow by communicating with the backend SDK -## The backend checks if the email exists and then sends a recovery email. + + The email includes a security token obtained from the **SuperTokens** core. -## When the user accesses the recovery link, they get directed to the frontend application. + + The security token gets validated by the backend SDK. If successful, the SDK begins the process of registering a new credential. From here, the flow matches the one described in the previous sections. - + -![Sign in form UI for passkeys login](/img/webauthn-recover-account.png) +Passkey account recovery flow diff --git a/docs/authentication/passkeys/initial-setup.mdx b/docs/authentication/passkeys/initial-setup.mdx index 0bcc43cc63..602f8ee23b 100644 --- a/docs/authentication/passkeys/initial-setup.mdx +++ b/docs/authentication/passkeys/initial-setup.mdx @@ -1,16 +1,28 @@ --- -title: Initial Setup -hide_title: true -sidebar_position: 3 -page_type: tutorial -recipe: webauthn -category: passkeys +title: Set Up Passkey Authentication +description: Integrate standalone WebAuthn passkey authentication with the frontend and backend SDKs, Session recipe, and authentication routes. +sidebar: + label: Initial Setup + order: 3 --- + -import ReactRouterCallout from "/docs/_blocks/react-router-callout.mdx"; +## Passkey integration summary -# Quickstart +- This guide configures standalone passkey authentication, not passkeys as an MFA factor. +- WebAuthn is supported by the Node.js, Python, and Go backend SDKs. +- Configure the WebAuthn and Session recipes on both the frontend and backend, then expose and render the authentication routes. +- The backend recipe exposes the endpoints used by the frontend and communicates with SuperTokens Core to complete registration and authentication. + + + + +Add SuperTokens passkey authentication to this application. Inspect the existing stack and authentication setup, confirm that the backend SDK supports WebAuthn, and determine the deployment origin and relying-party configuration. Configure the frontend and backend WebAuthn and Session recipes, auth routes, HTTPS requirements, and fallback authentication where appropriate. Preserve existing conventions, do not commit secrets, and validate registration, authentication, cancellation, and unsupported-browser behavior. + ## Overview @@ -19,349 +31,220 @@ The tutorial creates a login flow, rendered by either the **Prebuilt UI** compon ## Before you start -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page please follow the tutorial and return here once you're done. +Passkeys may be unavailable because of browser, device, or authenticator support. Keep another authentication method or +an account-recovery path available. A user can also cancel the browser or platform prompt; treat cancellation as an +interrupted attempt, let the user retry, and do not report it as a successful sign-in or sign-up. -:::info SDK Support -WebAuthn (Passkeys) authentication is available in the **Node.js SDK** and **Python SDK**. -::: +WebAuthn is available only in a [secure context](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API), +so serve the frontend over HTTPS in production. Browsers also allow `http://localhost` for local development. +The relying party (RP) ID must equal the frontend hostname or be a registrable suffix of it. The expected origin must +exactly match the frontend origin, including its scheme and non-default port. If the frontend and API use different +hostnames, [configure these values explicitly](/authentication/passkeys/customization#backend-recipe-configuration) +instead of relying on values derived from the -## Steps +## Steps ### 1. Initialize the frontend SDK - - + - + - - + + #### 1.1 Add the `WebAuthn` recipe in your main configuration file. + + + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -// highlight-next-line import WebAuthn from "supertokens-auth-react/recipe/webauthn"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - websiteDomain: "...", - appName: "...", - }, - recipeList: [ - // highlight-start - WebAuthn.init(), - // highlight-end - Session.init() - ] + appInfo: { + apiDomain: "...", + websiteDomain: "...", + appName: "...", + }, + recipeList: [WebAuthn.init(), Session.init()], }); ``` + + + + #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. + + - - + + + + ```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; - -// highlight-next-line -import { WebauthnPreBuiltUI } from 'supertokens-auth-react/recipe/webauthn/prebuiltui'; +import React from "react"; +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; + +import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the ^{appInfo.websiteBasePath} route*/} - // highlight-next-line - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } } ``` - - - - - + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -// highlight-next-line -import { WebauthnPreBuiltUI } from 'supertokens-auth-react/recipe/webauthn/prebuiltui'; +import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { - render() { - // highlight-start - if (canHandleRoute([WebauthnPreBuiltUI])) { - // This renders the login UI on the ^{appInfo.websiteBasePath} route - return getRoutingComponent([WebauthnPreBuiltUI]) - } - - // highlight-end - - return ( - {/*Your app*/} - ); + render() { + if (canHandleRoute([WebauthnPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([WebauthnPreBuiltUI]); } + + return {/*Your app*/}; + } } ``` - - - - - -Add the `WebAuthn` recipe in your `AuthComponent`. - - ```tsx title="/app/auth/auth.component.ts" - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIWebAuthn from "supertokens-auth-react-script/recipe/webauthn"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document - ) { } - - ngAfterViewInit() { - this.loadScript('^{prebuiltUIVersion}'); - } - - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById('supertokens-script'); - if (script) { - script.remove(); - } - } - - private loadScript(src: string) { - const script = this.renderer.createElement('script'); - script.type = 'text/javascript'; - script.src = src; - script.id = 'supertokens-script'; - script.onload = () => { - supertokensUIInit({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ -// highlight-start - supertokensUIWebAuthn.init(), -// highlight-end - supertokensUISession.init(), - ], - }); - } - this.renderer.appendChild(this.document.body, script); - } - } - ``` - - -
- - - -Add the `WebAuthn` recipe in your `AuthView` file. - - ```tsx - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIWebAuthn from "supertokens-auth-react-script/recipe/webauthn"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - - - - ``` - - - - - -
- - -
- - + + + + + + + + + +:::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] +Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. + + + + + + + + + +```tsx +import React from "react"; -Call the SDK init function at the start of your application. -The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. +import { BrowserRouter, useRoutes } from "react-router-dom"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import * as reactRouterDom from "react-router-dom"; - - +function AppRoutes() { + const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI]); - - + const routes = useRoutes([ + ...authRoutes.map((route) => route.props), + // Include the rest of your app routes + ]); -```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import WebAuthn from 'supertokens-web-js/recipe/webauthn' + return routes; +} -SuperTokens.init({ - appInfo: { - apiDomain: "...", - apiBasePath: "...", - appName: "...", - }, - recipeList: [ - Session.init(), - WebAuthn.init(), - ], -}); +function App() { + return ( + + + + + + ); +} ``` + + + + + + + + + +::: + + + + - - - - - + -First, you need to add the recipe script tag. + -```html - -``` -You can initialize the SDK. +Call the SDK init function at the start of your application. +The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. + + ```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensWebAuthn from 'supertokens-web-js-script/recipe/webauthn' +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import WebAuthn from "supertokens-web-js/recipe/webauthn"; -supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - supertokensWebAuthn.init(), - ], +SuperTokens.init({ + appInfo: { + apiDomain: "...", + apiBasePath: "...", + appName: "...", + }, + recipeList: [Session.init(), WebAuthn.init()], }); ``` - - - - - - - - - - - - - - - - - + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; SuperTokens.init({ - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", + apiDomain: "", + apiBasePath: "/auth", }); ``` - - - - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -369,32 +252,26 @@ import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() - - SuperTokens.Builder(this, "^{appInfo.apiDomain}") - .apiBasePath("^{appInfo.apiBasePath}") + + SuperTokens.Builder(this, "") + .apiBasePath("/auth") .build() } } ``` - - - - - - - - + + ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}" + apiDomain: "", + apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error @@ -404,341 +281,399 @@ fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { return true } - + } ``` - - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", + apiDomain: "", + apiBasePath: "/auth", ); } ``` + + + + - - - - - - - -### 2. Add the passkeys UI +### 2. Add the passkeys UI #### 2.1 Add the sign up form - - + + + Create a form in which the user can input their email address. When the user submits the form, call the `registerCredentialWithSignUp` method like in the next code snippet. Under the hood, the method communicates with the backend SDK to fetch the registration options. Once the backend responds, it uses the browser's APIs to begin the registration process. -For a more detailed overview of the sign up flow check the [Important Concepts page](/docs/authentication/passkeys/important-concepts#signup). +For a more detailed overview of the sign-up flow check the [Important Concepts page](/authentication/passkeys/important-concepts#sign-up). + + + + + ```ts import { registerCredentialWithSignUp } from "supertokens-web-js/recipe/webauthn"; async function signUp(email: string) { - try { - let response = await registerCredentialWithSignUp({ - email - }); - - if ( - response.status === "SIGN_UP_NOT_ALLOWED" || - response.status === "INVALID_AUTHENTICATOR_ERROR" - ) { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else if ( - response.status === "INVALID_EMAIL_ERROR" || - response.status === "EMAIL_ALREADY_EXISTS_ERROR" - ) { - window.alert("Invalid email"); - } else if ( - response.status === "INVALID_CREDENTIALS_ERROR" || - response.status === "OPTIONS_NOT_FOUND_ERROR" || - response.status === "INVALID_OPTIONS_ERROR" || - response.status === "INVALID_AUTHENTICATOR_ERROR" || - response.status === "EMAIL_ALREADY_EXISTS_ERROR" || - response.status === "AUTHENTICATOR_ALREADY_REGISTERED" || - response.status === "FAILED_TO_REGISTER_USER" || - response.status === "WEBAUTHN_NOT_SUPPORTED" - ) { - // These errors represent various issues with the authenticator, credential or the flow itself. - // These should be handled individually by you. - // The user should be informed that they should retry the sign up process or get in touch with you. - window.alert("Please try again"); - } else { - // User signed up successfully. - window.alert("You have been signed up successfully"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } + try { + let response = await registerCredentialWithSignUp({ + email, + userContext: {}, + }); + + if (response.status === "SIGN_UP_NOT_ALLOWED" || response.status === "INVALID_AUTHENTICATOR_ERROR") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else if (response.status === "INVALID_EMAIL_ERROR" || response.status === "EMAIL_ALREADY_EXISTS_ERROR") { + window.alert("Invalid email"); + } else if ( + response.status === "INVALID_CREDENTIALS_ERROR" || + response.status === "OPTIONS_NOT_FOUND_ERROR" || + response.status === "INVALID_OPTIONS_ERROR" || + response.status === "AUTHENTICATOR_ALREADY_REGISTERED" || + response.status === "FAILED_TO_REGISTER_USER" || + response.status === "WEBAUTHN_NOT_SUPPORTED" + ) { + // These errors represent various issues with the authenticator, credential or the flow itself. + // These should be handled individually by you. + // The user should be informed that they should retry the sign up process or get in touch with you. + window.alert("Please try again"); + } else if (response.status === "INVALID_GENERATED_OPTIONS_ERROR") { + window.alert("The registration request expired. Please try again."); + } else if (response.status === "GENERAL_ERROR") { + window.alert(response.message); + } else if (response.status === "OK") { + // User signed up successfully. + window.alert("You have been signed up successfully"); + } else { + window.alert("Sign up could not be completed. Please try another authentication method."); } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } } ``` - + + + + + + + - +The requests in this standalone authentication flow omit `shouldTryLinkingWithSessionUser`, so it defaults to `false`. +Set it to `true` only for an authenticated add-factor or account-linking flow where your backend policy permits linking +to the session user. - ## Get the email address from the user - Add a form where the user can input their email address. - - ## Fetch the registration options from the backend SDK - - When the user submits the form, call the `register options` API. - Save the response to use it in the next step. - - ```bash - curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/webauthn/register/options' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - "email": "johndoe@gmail.com", - "displayName": "John Doe" - }' - ``` - - :::caution - The returned result matches the format required by the **browser's WebAuthn API**. - You will have to map the properties to the correct format based on the requirements of your platform. - ::: - - ## Register a new credential authenticator API - - Use the received options generate a new credential. - The implementation will vary based on the platform you are using. - - **React Native**: You can use the [`react-native-passkey`](https://github.com/f-23/react-native-passkey) library. - - **iOS**: Use the [`Authentication Services`](https://developer.apple.com/documentation/authenticationservices) framework. - - **Android**: Use the [`Android Credential Manager API`](https://developer.android.com/identity/sign-in/credential-manager). - - **Flutter**: Use [platform channels](https://docs.flutter.dev/platform-integration/platform-channels#architecture) to access the native APIs. - - ## Call the sign up API - - Using the newly generate credential, call the sign up API to save the new authentication method. - - ```bash - curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/webauthn/signup' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - "webauthnGeneratedOptionsId": "opt_123...", - "credential": { - "id": "credential_id", - "rawId": "raw_credential_id", - "response": { - "clientDataJSON": "base64_client_data_json", - "attestationObject": "base64_attestation_object" - }, - "type": "public-key" + +Add a form where the user can input their email address. + + + + +When the user submits the form, call the `register options` API. +Save the response to use it in the next step. + + + +```bash +curl --location --request POST '/auth/webauthn/options/register' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "email": "johndoe@gmail.com", + "displayName": "John Doe" +}' +``` + + + +:::warning[The returned result matches the format required by a WebAuthn client API.] +You will have to map the properties to the correct format based on the requirements of your platform. +::: + + + + +Use the received options generate a new credential. +The implementation will vary based on the platform you are using. +- **React Native**: You can use the [`react-native-passkey`](https://github.com/f-23/react-native-passkey) library. +- **iOS**: Use the [`Authentication Services`](https://developer.apple.com/documentation/authenticationservices) framework. +- **Android**: Use the [`Android Credential Manager API`](https://developer.android.com/identity/sign-in/credential-manager). +- **Flutter**: Use [platform channels](https://docs.flutter.dev/platform-integration/platform-channels#architecture) to access the native APIs. + + + + +Using the newly generated credential, call the sign up API to save the new authentication method. + + + +Encode `id`, `rawId`, `clientDataJSON`, and `attestationObject` as unpadded Base64URL. Include `transports` when the +authenticator supplies it. + - + + + - #### 2.2 Add the login form - - + + + Add a button that can trigger the sign in flow. This is all that you need in terms of UI. When the user clicks it, call the `authenticateCredentialWithSignIn` method to handle the whole process. -The function uses the backend registration options to trigger the challenge signing action through the browser API. +The function uses the backend authentication options to trigger the challenge signing action through the browser API. Then, it forwards the result to the backend for validation. -For a more detailed overview of the login flow check the [Important Concepts page](/docs/authentication/passkeys/important-concepts#login). +For a more detailed overview of the login flow check the [Important Concepts page](/authentication/passkeys/important-concepts#login). + + + + + ```ts import { authenticateCredentialWithSignIn } from "supertokens-web-js/recipe/webauthn"; -async function signIn(email: string) { - try { - let response = await authenticateCredentialWithSignIn(); - - if ( - response.status === "SIGN_IN_NOT_ALLOWED" - ) { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else if (response.status === "WEBAUTHN_NOT_SUPPORTED") { - // the user's browser does not support the WebAuthn standard - window.alert("Login method not supported"); - } else if ( - response.status === "INVALID_CREDENTIALS_ERROR" || - response.status === "FAILED_TO_AUTHENTICATE_USER" - ) { - // These errors represent various issues with the authenticator, credential or the flow itself. - // These should be handled individually by you. - // The user should be informed that they should retry the sign in process or get in touch with you. - window.alert("Please try again"); - } else { - // User signed in successfully. - window.alert("You have been signed in successfully"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } +async function signIn() { + try { + let response = await authenticateCredentialWithSignIn({ userContext: {} }); + + if (response.status === "SIGN_IN_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else if (response.status === "WEBAUTHN_NOT_SUPPORTED") { + // the user's browser does not support the WebAuthn standard + window.alert("Login method not supported"); + } else if ( + response.status === "INVALID_CREDENTIALS_ERROR" || + response.status === "INVALID_OPTIONS_ERROR" || + response.status === "FAILED_TO_AUTHENTICATE_USER" + ) { + // These errors represent various issues with the authenticator, credential or the flow itself. + // FAILED_TO_AUTHENTICATE_USER can also indicate that the user cancelled the authenticator prompt. + // These should be handled individually by you. + // The user should be informed that they should retry the sign in process or get in touch with you. + window.alert("Please try again"); + } else if (response.status === "GENERAL_ERROR") { + window.alert(response.message); + } else if (response.status === "OK") { + // User signed in successfully. + window.alert("You have been signed in successfully"); + } else { + window.alert("Sign in could not be completed. Please try another authentication method."); } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } } ``` - + + + + - + + - ## Add a button that can trigger the sign in flow - - ## Get the sign in options from the backend SDK - When the user taps the sign in button, call the backend API to fetch the sign in options. - - ```bash - curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/webauthn/signin/options' \ - --header 'Content-Type: application/json; charset=utf-8' - ``` - - ## Use the authenticator to sign the challenge + + - With the received options, invoke the authenticator to sign the challenge. - The implementation will vary based on the platform you are using. + +When the user taps the sign in button, call the backend API to fetch the sign in options. - ## Call the sign in API - - Send the signed challenge to the backend for validation. - - ```bash - curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/webauthn/signin' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - "webauthnGeneratedOptionsId": "opt_123...", - "credential": { - "id": "credential_id", - "rawId": "raw_credential_id", - "response": { - "clientDataJSON": "base64_client_data_json", - "attestationObject": "base64_attestation_object" - }, - "type": "public-key" + + +```bash +curl --location --request POST '/auth/webauthn/options/signin' \ +--header 'Content-Type: application/json; charset=utf-8' +``` + + + + + + +With the received options, invoke the authenticator to sign the challenge. +The implementation will vary based on the platform you are using. + + + + +Send the signed challenge to the backend for validation. + + + +Encode `id`, `rawId`, `clientDataJSON`, `authenticatorData`, `signature`, and an optional `userHandle` as unpadded +Base64URL. Include `userHandle` in `credential.response` when the authenticator returns it. + - + + + - - + - + ### 2. Initialize the backend SDK - + - + ### 3. Initialize the backend SDK - + Initialize the backend SDK and include the **WebAuthn** `recipe`. -The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. +The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. The recipe exposes the required endpoints that get accessed by the frontend code, and communicates with the **SuperTokens Core** to complete the authentication flow. -You can [configure different aspects](/docs/authentication/passkeys/customization) of the recipe's behavior but, for the completion of this guide, use the default values. -After you confirm that the flow works as expected you can explore more advanced customisations options. - - - - - +You can [configure different aspects](/authentication/passkeys/customization) of the recipe's behavior but, for the completion of this guide, use the default values. +After you confirm that the flow works as expected, you can explore more advanced customization options. + + ```ts import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import WebAuthN from "supertokens-node/recipe/webauthn"; supertokens.init({ - // Replace this with the framework you are using - framework: "express", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io", - // apiKey: - }, - appInfo: { - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "/auth", - websiteBasePath: "/auth", - }, - recipeList: [ - WebAuthN.init(), - Session.init() - ] + // Replace this with the framework you are using + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [WebAuthN.init(), Session.init()], }); ``` - - - - - -:::caution - -At the moment there is no support for using passkeys authentication in the Go SDK. - -::: - - - - + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/webauthn" + "github.com/supertokens/supertokens-golang/supertokens" +) +func main() { + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "https://try.supertokens.io", + // APIKey: "", + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + }, + RecipeList: []supertokens.Recipe{ + webauthn.Init(nil), + session.Init(nil), + }, + }) + if err != nil { + panic(err) + } +} +``` + + ```python from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, webauthn @@ -765,51 +700,29 @@ init( ] ) ``` + + - - - - - + ## Next steps Having completed the main setup, you can explore more advanced topics related to the **WebAuthn** recipe. - - - - Backend Recipe Configuration - - - Read through the configuration options for the backend recipe. - - - - - Customize Credentials Generation - - - See how you can adjust the process that generates credentials. - - - - - Customize Credentials Validation - - - Discover how to customize the validation process. - - - - - Email Delivery - - - Customize the email sending process. - - - - - - + + +Read through the configuration options for the backend recipe. + + +See how you can adjust the process that generates credentials. + + +Discover how to customize the validation process. + + +Customize the email sending process. + + + + + diff --git a/docs/authentication/passkeys/introduction.mdx b/docs/authentication/passkeys/introduction.mdx index 14fd03d3b6..bb7ef6e247 100644 --- a/docs/authentication/passkeys/introduction.mdx +++ b/docs/authentication/passkeys/introduction.mdx @@ -1,78 +1,63 @@ --- -title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true -page_type: overview -recipe: webauthn -category: passkeys +title: Passkey Authentication +description: Use WebAuthn passkeys as standalone passwordless authentication with biometrics, security keys, or device credentials. +sidebar: + label: Overview + order: 1 --- -# Passkeys authentication + + +## Passkey authentication summary + +- This section covers passkeys as a standalone passwordless authentication method based on WebAuthn, not passkeys used as an MFA factor. +- Users authenticate with a compatible authenticator, using a PIN, biometrics, a security key, or a cross-device flow instead of a password. +- WebAuthn requires a secure context and an RP ID and origin that match the deployment. +- Credential generation and credential validation can be customized. + + ## Overview -**Passkeys** is a passwordless authentication method that follows the **WebAuthn**, Web Authentication, specification. +**Passkeys** are a passwordless authentication method based on the **WebAuthn**, Web Authentication, specification. The standard allows users to log in using biometric authentication, security keys, or device-based credentials, replacing traditional username and password combinations. -Sign in form UI for passkeys login + + Sign in form UI for passkeys login + ## Prerequisites -To implement the feature you need to make sure that your users have compatible devices with either **built-in biometric sensors**, or **external security keys**. +Users need a compatible browser and authenticator. Depending on the authenticator, they may use a PIN, biometrics, a security key, or a cross-device flow. +WebAuthn is available only in a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), normally HTTPS, with limited exceptions such as localhost. +Your relying party (RP) ID and origin must also match the frontend deployment; see the [customization guidance](/authentication/passkeys/customization#backend-recipe-configuration). ## Getting started -Before going into the actual quickstart guide, read through the [**Important Concepts** page](/docs/authentication/passkeys/important-concepts). +Before going into the actual quickstart guide, read through the [**Important Concepts** page](/authentication/passkeys/important-concepts). It provides a high-level overview of the terms and concepts used in the passkeys authentication flow. - - - - Important Concepts - - - Go through a quick summary of the WebAuthn specifications to get accustomed with the language used in the guides. - - - - - Initial Setup - - - Implement an authentication flow that uses passkeys to login users. - - - - -## Customization - -To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - - Backend Recipe Configuration - - - Read through all the options that you can set during the initialization step. - - - - - Credential Generation - - - See how you can adjust the process that generates credentials. - - - - - Credential Validation - - - Discover how to customize the validation process. - - - - + + +Go through a quick summary of the WebAuthn specifications to get accustomed with the language used in the guides. + + +Implement an authentication flow that uses passkeys to log in users. + + + +## Customization + +To adjust the functionality to fit your use case you can explore different sections from the documentation. + + + +Read through all the options that you can set during the initialization step. + + +See how you can adjust the process that generates credentials. + + +Discover how to customize the validation process. + + diff --git a/docs/authentication/passkeys/meta.ts b/docs/authentication/passkeys/meta.ts new file mode 100644 index 0000000000..3b9fd61a6e --- /dev/null +++ b/docs/authentication/passkeys/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Passkeys", + icon: "fingerprint", + order: 80, +}); diff --git a/docs/authentication/passwordless/_blocks/backend-sdk-init.mdx b/docs/authentication/passwordless/_blocks/backend-sdk-init.mdx deleted file mode 100644 index b18eb32ef7..0000000000 --- a/docs/authentication/passwordless/_blocks/backend-sdk-init.mdx +++ /dev/null @@ -1,146 +0,0 @@ - -You need to initialize the **Backend SDK** alongside the code that starts your server. -The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. - -For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. -Click one of the options from the next form and the code snippet updates. - - - - - - - -```tsx title="Backend SDK Init" showAppTypeSelect -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -// highlight-next-line -import Passwordless from "supertokens-node/recipe/passwordless"; - -supertokens.init({ - // Replace this with the framework you are using - framework: "express", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - // highlight-start - Passwordless.init({ - flowType: "^{recipes.passwordless.flowType}", - contactMethod: "^{recipes.passwordless.contactMethod}" - }), - // highlight-end - Session.init() - ] -}); -``` - - - - - - - - - - - -```python title="Backend SDK Init" showAppTypeSelect -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import passwordless, session -# highlight-next-line -^{derived.pythonContactMethodImport} - -init( - app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" - ), - supertokens_config=SupertokensConfig( - # We use try.supertokens for demo purposes. - # At the end of the tutorial we will show you how to create - # your own SuperTokens core instance and then update your config. - connection_uri="https://try.supertokens.io^{derived.appIdPathname}", - # api_key: - ), - framework='fastapi', - recipe_list=[ - session.init(), # initializes session features - passwordless.init( - flow_type="^{recipes.passwordless.flowType}", - contact_config=^{derived.pythonContactMethodMethod}() - ) - ], - mode='asgi' # use wsgi if you are running using gunicorn -) -``` - - - - - - - - - -```go title="Backend SDK Init" showAppTypeSelect -import ( - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/passwordless" - "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - apiBasePath := "^{appInfo.apiBasePath}" - websiteBasePath := "^{appInfo.websiteBasePath}" - err := supertokens.Init(supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - ConnectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // APIKey: - }, - AppInfo: supertokens.AppInfo{ - AppName: "^{appInfo.appName}", - APIDomain: "^{appInfo.apiDomain}", - WebsiteDomain: "^{appInfo.websiteDomain}", - APIBasePath: &apiBasePath, - WebsiteBasePath: &websiteBasePath, - }, - RecipeList: []supertokens.Recipe{ - passwordless.Init(plessmodels.TypeInput{ - FlowType: "^{recipes.passwordless.flowType}", - ^{derived.goPasswordlessContactMethodMethod} - }), - session.Init(nil), // initializes session features - }, - }) - - if err != nil { - panic(err.Error()) - } -} -``` - - - - - - diff --git a/docs/authentication/passwordless/_blocks/frontend-custom-ui-magic-link-ui.mdx b/docs/authentication/passwordless/_blocks/frontend-custom-ui-magic-link-ui.mdx deleted file mode 100644 index 47bbdbc1d2..0000000000 --- a/docs/authentication/passwordless/_blocks/frontend-custom-ui-magic-link-ui.mdx +++ /dev/null @@ -1,493 +0,0 @@ - -The following section shows you what aspects you need to cover to implement the UI for a `Magic Link` flow. -The same flow applies during either sign up or sign in. -This guide shows you how to determine if the system creates a new user in the next steps. - -#### 2.1 Sending the Magic link - -You need to add a form that asks the user for their email address or phone number. -When the user submits the form, you need to call the following API to create and send them a **Magic Link**. - -:::info - -You configure the contact method on the next page, where you discuss the process of adding the `SDK` to your backend app. - -::: - - - - - - - - - -```tsx -import { createCode } from "supertokens-web-js/recipe/passwordless"; - -async function sendMagicLink(email: string) { - try { - let response = await createCode({ - email - }); - /** - * For phone number, use this: - - let response = await createCode({ - phoneNumber: "+1234567890" - }); - - */ - - if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function sendMagicLink(email: string) { - try { - let response = await supertokensPasswordless.createCode({ - email - }); - /** - * For phone number, use this: - - let response = await supertokens^{recipeNameCapitalLetters}.createCode({ - phoneNumber: "+1234567890" - }); - - */ - - if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // Magic link sent successfully. - window.alert("Please check your email for the magic link"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - - -For email based login - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "email": "johndoe@gmail.com" -}' -``` - -For phone number based login -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "phoneNumber": "+1234567890" -}' -``` - - -The response body from the API call has a `status` property in it: -- `status: "OK"`: This means that the magic link was successfully sent. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -The response from the API call is the following object (in case of `status: "OK"`): -```json -{ - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - fetchResponse: Response; // raw fetch response from the API call -} -``` - -You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: -- Resend a new magic link. -- Detect if the user has already sent a magic link before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the resend link button. - - - - - -#### 2.2 Resending a magic link - -After sending the initial magic link to the user, you may want to display a resend button to them. -When the user clicks on this button, you should call the following API - - - - - - - -```tsx -import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function resendMagicLink() { - try { - let response = await resendCode(); - - if (response.status === "RESTART_FLOW_ERROR") { - // this can happen if the user has already successfully logged in into - // another device whilst also trying to login to this one. - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } else { - // Magic link resent successfully. - window.alert("Please check your email for the magic link"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function resendMagicLink() { - try { - let response = await supertokensPasswordless.resendCode(); - - if (response.status === "RESTART_FLOW_ERROR") { - // this can happen if the user has already successfully logged in into - // another device whilst also trying to login to this one. - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await supertokensPasswordless.clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } else { - // Magic link resent successfully. - window.alert("Please check your email for the magic link"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code/resend' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "deviceId": "...", - "preAuthSessionId": "...." -}' -``` - - -The response body from the API call has a `status` property in it: -- `status: "OK"`: This means that the magic link was successfully sent. -- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - - - - - -#### How to detect if the user is on step 2.1 or step 2.2? - -If you are building the UI for both of the previous steps on the same page you might run into an issue when the user refreshes the page. -To prevent this you need a way to know which UI to show. - - - - - - - -```tsx -import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function hasInitialMagicLinkBeenSent() { - return await getLoginAttemptInfo() !== undefined; -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function hasInitialMagicLinkBeenSent() { - return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; -} -``` - - - - -If `hasInitialMagicLinkBeenSent` returns `true`, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number. - - - - - -Since you save the `preAuthSessionId` and `deviceId` after sending the initial magic link, you can know if the user is on either **step 3.1** or **step 3.2**. Check if these tokens are on the device. - -If they aren't, you should follow **step 3.1**, else follow **step 3.2**. - -:::important -You need to clear these tokens if: -- the user navigates away from the **step 3.2** page -- you get a `RESTART_FLOW_ERROR` at any point in time from an API call -- the user has successfully logged in. -::: - - - - - -#### 2.3 Consuming the magic link - -When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow. -To do this you ca use this code sample. - - - - - - - -```tsx -import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function isThisSameBrowserAndDevice() { - return await getLoginAttemptInfo() !== undefined; -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function isThisSameBrowserAndDevice() { - return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; -} -``` - - - - - - - - -Since you save the `preAuthSessionId` and `deviceId`, you can check if they exist on the app. If they do, then it's the same device that the user has opened the link on, else it's a different device. - - - - - -:::important - -Add a intermediate step if the user came from a different device. - -::: - -If the user clicked on a link from a different device, you need to show some kind of an intermediate UI. -This is to protect against email clients opening the magic link on their servers and consuming the link. - -The page should require additional user interaction before consuming the magic link. -For example, you could show a button with the following text: `Click here to login into this device`. -On click, you can consume the magic link to log the user into that device. - -With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link. - - - - - - - -```tsx -import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function handleMagicLinkClicked() { - try { - let response = await consumeCode(); - - if (response.status === "OK") { - // we clear the login attempt info that was added when the createCode function - // was called since the login was successful. - await clearLoginAttemptInfo(); - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // user sign up success - } else { - // user sign in success - } - window.location.assign("/home") - } else { - // this can happen if the magic link has expired or is invalid - // or if it was denied due to security reasons in case of automatic account linking - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function handleMagicLinkClicked() { - try { - let response = await supertokensPasswordless.consumeCode(); - - if (response.status === "OK") { - // we clear the login attempt info that was added when the createCode function - // was called since the login was successful. - await supertokensPasswordless.clearLoginAttemptInfo(); - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // user sign up success - } else { - // user sign in success - } - window.location.assign("/home") - } else { - // this can happen if the magic link has expired or is invalid - // or if it was denied due to security reasons in case of automatic account linking - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await supertokensPasswordless.clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - -You need to remove the `linkCode` and `preAuthSessionId` from the Magic link. For example, if the Magic link is - -```text -https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs= -``` - -Then the `preAuthSessionId` is the value of the query parameter `preAuthSessionId` (`PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=` in the example), and the `linkCode` is the part after the `#` (`s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=` in the example). - -We can then use these to call the consume API - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code/consume' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=", - "preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=" -}' -``` - - -:::info Multi Tenancy -For a multi-tenancy setup, the `` value can fetch from the `tenantId` query parameter from the magic link. If it's not there in the link, you can use the value `"public"` (which is the default tenant). -::: - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - - - - diff --git a/docs/authentication/passwordless/_blocks/frontend-custom-ui-otp-ui.mdx b/docs/authentication/passwordless/_blocks/frontend-custom-ui-otp-ui.mdx deleted file mode 100644 index 3487b9ad64..0000000000 --- a/docs/authentication/passwordless/_blocks/frontend-custom-ui-otp-ui.mdx +++ /dev/null @@ -1,444 +0,0 @@ - -The following section shows you what aspects you need to cover to implement the UI for a `OTP`, One-Time Password, flow. -The same flow applies during either sign up or sign in. -This guide shows you how to determine if you create a new user in the next steps. - -#### 2.1 Creating and sending the OTP - -You have to add a form that asks the user for their email address or phone number. -When the users submit the form you have to call the following API to create and send them an OTP. - - - - - - - -```tsx -import { createCode } from "supertokens-web-js/recipe/passwordless"; - -async function sendOTP(email: string) { - try { - let response = await createCode({ - email - }); - /** - * For phone number, use this: - - let response = await createPasswordlessCode({ - phoneNumber: "+1234567890" - }); - - */ - - if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // OTP sent successfully. - window.alert("Please check your email for an OTP"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function sendOTP(email: string) { - try { - let response = await supertokensPasswordless.createCode({ - email - }); - /** - * For phone number, use this: - - let response = await supertokens^{recipeNameCapitalLetters}.createCode({ - phoneNumber: "+1234567890" - }); - - */ - - if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // OTP sent successfully. - window.alert("Please check your email for an OTP"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you, - // or if the input email / phone number is not valid. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - - -For email based login - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "email": "johndoe@gmail.com" -}' -``` - -For phone number based login -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "phoneNumber": "+1234567890" -}' -``` - - -The response body from the API call has a `status` property in it: -- `status: "OK"`: This means that the OTP was successfully sent. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -The response from the API call is the following object (in case of `status: "OK"`): -```json -{ - status: "OK"; - deviceId: string; - preAuthSessionId: string; - flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - fetchResponse: Response; // raw fetch response from the API call -} -``` - -You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: -- Resend a new OTP. -- Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button. -- Verify the user's input OTP. - - - - - -#### 2.2 Resending a OTP - -After you send the OTP to the user, you may want to display a resend button to them. -When the user clicks on this button, you should call the following API - - - - - - - -```tsx -import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function resendOTP() { - try { - let response = await resendCode(); - - if (response.status === "RESTART_FLOW_ERROR") { - // this can happen if the user has already successfully logged in into - // another device whilst also trying to login to this one. - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } else { - // OTP resent successfully. - window.alert("Please check your email for the OTP"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function resendOTP() { - try { - let response = await supertokensPasswordless.resendCode(); - - if (response.status === "RESTART_FLOW_ERROR") { - // this can happen if the user has already successfully logged in into - // another device whilst also trying to login to this one. - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await supertokensPasswordless.clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } else { - // OTP resent successfully. - window.alert("Please check your email for the OTP"); - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code/resend' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "deviceId": "...", - "preAuthSessionId": "...." -}' -``` - - -The response body from the API call has a `status` property in it: -- `status: "OK"`: This means that the OTP was successfully sent. -- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. - - - - - -#### How to detect if the user is on step 3.1 or step 3.2? - -If you are building the UI for both of the previous steps on the same page you might run into an issue when the user refreshes the page. -To prevent this you need a way to know which UI to show. - - - - - - - -```tsx -import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function hasInitialOTPBeenSent() { - return await getLoginAttemptInfo() !== undefined; -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function hasInitialOTPBeenSent() { - return await supertokensPasswordless.getLoginAttemptInfo() !== undefined; -} -``` - - - - -If `hasInitialOTPBeenSent` returns `true`, it means that the user has already sent the initial OTP to themselves, and you can show the enter OTP form + resend OTP button 3.2. Else show a form asking them to enter their email / phone number 3.1. - - - - - -Since you save the `preAuthSessionId` and `deviceId` after you send the initial OTP, you can determine if the user is in step 3.1 or 3.2. -Check if you stored these tokens on the device. - -If they aren't, you should follow 3.1, else follow 3.2. - -:::important -You need to clear these tokens if: -- the user navigates away from the 3.2 page -- you get a `RESTART_FLOW_ERROR` at any point in time from an API call -- the user has successfully logged in. -::: - - - - - -#### 2.3 Verifying the OTP - -When the user enters an OTP you have to call the following API to verify it - - - - - - - -```tsx -import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - -async function handleOTPInput(otp: string) { - try { - let response = await consumeCode({ - userInputCode: otp - }); - - if (response.status === "OK") { - // we clear the login attempt info that was added when the createCode function - // was called since the login was successful. - await clearLoginAttemptInfo(); - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // user sign up success - } else { - // user sign in success - } - window.location.assign("/home") - } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { - // the user entered an invalid OTP - window.alert("Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount)); - } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { - // it can come here if the entered OTP was correct, but has expired because - // it was generated too long ago. - window.alert("Old OTP entered. Please regenerate a new one and try again"); - } else { - // this can happen if the user tried an incorrect OTP too many times. - // or if it was denied due to security reasons in case of automatic account linking - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensPasswordless from "supertokens-web-js-script/recipe/passwordless"; -async function handleOTPInput(otp: string) { - try { - let response = await supertokensPasswordless.consumeCode({ - userInputCode: otp - }); - - if (response.status === "OK") { - // we clear the login attempt info that was added when the createCode function - // was called since the login was successful. - await supertokensPasswordless.clearLoginAttemptInfo(); - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // user sign up success - } else { - // user sign in success - } - window.location.assign("/home") - } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { - // the user entered an invalid OTP - window.alert("Wrong OTP! Please try again. Number of attempts left: " + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount)); - } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { - // it can come here if the entered OTP was correct, but has expired because - // it was generated too long ago. - window.alert("Old OTP entered. Please regenerate a new one and try again"); - } else { - // this can happen if the user tried an incorrect OTP too many times. - // or if it was denied due to security reasons in case of automatic account linking - - // we clear the login attempt info that was added when the createCode function - // was called - so that if the user does a page reload, they will now see the - // enter email / phone UI again. - await supertokensPasswordless.clearLoginAttemptInfo(); - window.alert("Login failed. Please try again"); - window.location.assign("/auth") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - - - - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup/code/consume' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "deviceId": "...", - "preAuthSessionId": "...", - "userInputCode": "" -}' -``` - - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "INCORRECT_USER_INPUT_CODE_ERROR"`: The entered OTP is invalid. The response contains information about the maximum number of retries and the number of failed attempts. -- `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. -- `status: "RESTART_FLOW_ERROR"`: These responses that the user tried invalid OTPs too many times. -- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - - - - - diff --git a/docs/authentication/passwordless/_category_.json b/docs/authentication/passwordless/_category_.json deleted file mode 100644 index 5fa7d0559c..0000000000 --- a/docs/authentication/passwordless/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Passwordless", - "position": 2 -} diff --git a/docs/authentication/passwordless/allow-list-flow.mdx b/docs/authentication/passwordless/allow-list-flow.mdx index 18692342b7..1c472eaac5 100644 --- a/docs/authentication/passwordless/allow-list-flow.mdx +++ b/docs/authentication/passwordless/allow-list-flow.mdx @@ -1,18 +1,10 @@ --- title: Implement allow list based sign up -hide_title: true -description: >- - Discover how to implement an allow list based sign up flow with the - passwordless recipe. -sidebar_position: 11 -page_type: guide -recipe: passwordless -category: passwordless +description: Discover how to implement an allow list based sign up flow with the passwordless recipe. +sidebar: + order: 80 --- - -# Passwordless login via allow list - ## Overview In this flow, you create a list of emails or phone numbers that are allowed to sign up. @@ -21,12 +13,12 @@ Based on that users can go through the passwordless flow. ## Before you start This guide assumes that you already have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +If you have not, please check the [Quickstart Guide](/quickstart). ### Prerequisites This guide uses the `UserMetadata` recipe to store the allow list. -You need to [enable it](/docs/post-authentication/user-management/user-metadata) in the SDK initialization step. +You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps @@ -34,80 +26,100 @@ You need to [enable it](/docs/post-authentication/user-management/user-metadata) ### 1. Add a way to keep track of allowed emails or phone numbers Start by maintaining an allow list of emails. -You can either store this list in your own database, or use the metadata feature provided by SuperTokens to store this. -This may seem like a strange use case of the user metadata recipe provided, but it works. +Use transactional application storage for a production allow list. The User Metadata examples below are suitable for a simple prototype, but their read-modify-write updates are not atomic: concurrent additions can overwrite each other. The following code samples show you how to save the allow list in the user metadata. - - - + + ```tsx -import UserMetadata from "supertokens-node/recipe/usermetadata" - -async function addEmailToAllowlist(email: string) { - let existingData = await UserMetadata.getUserMetadata("emailAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - allowList = [...allowList, email]; - await UserMetadata.updateUserMetadata("emailAllowList", { - allowList - }); +import UserMetadata from "supertokens-node/recipe/usermetadata"; + +async function addEmailToAllowlist(tenantId: string, email: string) { + const metadataKey = `${tenantId}:emailAllowList`; + let existingData = await UserMetadata.getUserMetadata(metadataKey); + let allowList: string[] = existingData.metadata.allowList || []; + allowList = [...allowList, email]; + await UserMetadata.updateUserMetadata(metadataKey, { + allowList, + }); } -async function isEmailAllowed(email: string) { - let existingData = await UserMetadata.getUserMetadata("emailAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - return allowList.includes(email); +async function isEmailAllowed(tenantId: string, email: string) { + let existingData = await UserMetadata.getUserMetadata(`${tenantId}:emailAllowList`); + let allowList: string[] = existingData.metadata.allowList || []; + return allowList.includes(email); } -async function addPhoneNumberToAllowlist(phoneNumber: string) { - let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - allowList = [...allowList, phoneNumber]; - await UserMetadata.updateUserMetadata("phoneNumberAllowList", { - allowList - }); +async function addPhoneNumberToAllowlist(tenantId: string, phoneNumber: string) { + const metadataKey = `${tenantId}:phoneNumberAllowList`; + let existingData = await UserMetadata.getUserMetadata(metadataKey); + let allowList: string[] = existingData.metadata.allowList || []; + allowList = [...allowList, phoneNumber]; + await UserMetadata.updateUserMetadata(metadataKey, { + allowList, + }); } -async function isPhoneNumberAllowed(phoneNumber: string) { - let existingData = await UserMetadata.getUserMetadata("phoneNumberAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - return allowList.includes(phoneNumber); +async function isPhoneNumberAllowed(tenantId: string, phoneNumber: string) { + let existingData = await UserMetadata.getUserMetadata(`${tenantId}:phoneNumberAllowList`); + let allowList: string[] = existingData.metadata.allowList || []; + return allowList.includes(phoneNumber); } ``` + + +```go +import ( + "fmt" - - + "github.com/supertokens/supertokens-golang/recipe/usermetadata" +) -```go -import "github.com/supertokens/supertokens-golang/recipe/usermetadata" +func stringListFromMetadata(value interface{}) ([]string, error) { + if value == nil { + return []string{}, nil + } + items, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf("allowList metadata is not an array") + } + result := make([]string, 0, len(items)) + for _, item := range items { + text, ok := item.(string) + if !ok { + return nil, fmt.Errorf("allowList metadata contains a non-string value") + } + result = append(result, text) + } + return result, nil +} -func addEmailToAllowlist(email string) error { - existingData, err := usermetadata.GetUserMetadata("emailAllowList") +func addEmailToAllowlist(tenantId, email string) error { + metadataKey := tenantId + ":emailAllowList" + existingData, err := usermetadata.GetUserMetadata(metadataKey) if err != nil { return err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata + allowList, err := stringListFromMetadata(existingData["allowList"]) + if err != nil { + return err } allowList = append(allowList, email) - _, err = usermetadata.UpdateUserMetadata("emailAllowList", map[string]interface{}{ + _, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{ "allowList": allowList, }) return err } -func isEmailAllowed(email string) (bool, error) { - existingData, err := usermetadata.GetUserMetadata("emailAllowList") +func isEmailAllowed(tenantId, email string) (bool, error) { + existingData, err := usermetadata.GetUserMetadata(tenantId + ":emailAllowList") if err != nil { return false, err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata + allowList, err := stringListFromMetadata(existingData["allowList"]) + if err != nil { + return false, err } for _, allowedEmail := range allowList { if allowedEmail == email { @@ -117,32 +129,31 @@ func isEmailAllowed(email string) (bool, error) { return false, nil } -func addPhoneNumberToAllowlist(phoneNumber string) error { - existingData, err := usermetadata.GetUserMetadata("phoneNumberAllowList") +func addPhoneNumberToAllowlist(tenantId, phoneNumber string) error { + metadataKey := tenantId + ":phoneNumberAllowList" + existingData, err := usermetadata.GetUserMetadata(metadataKey) if err != nil { return err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata + allowList, err := stringListFromMetadata(existingData["allowList"]) + if err != nil { + return err } allowList = append(allowList, phoneNumber) - _, err = usermetadata.UpdateUserMetadata("phoneNumberAllowList", map[string]interface{}{ + _, err = usermetadata.UpdateUserMetadata(metadataKey, map[string]interface{}{ "allowList": allowList, }) return err } -func isPhoneNumberAllowed(phoneNumber string) (bool, error) { - existingData, err := usermetadata.GetUserMetadata("phoneNumberAllowList") +func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) { + existingData, err := usermetadata.GetUserMetadata(tenantId + ":phoneNumberAllowList") if err != nil { return false, err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata + allowList, err := stringListFromMetadata(existingData["allowList"]) + if err != nil { + return false, err } for _, allowedPhoneNumber := range allowList { if allowedPhoneNumber == phoneNumber { @@ -152,10 +163,8 @@ func isPhoneNumberAllowed(phoneNumber string) (bool, error) { return false, nil } ``` - - - - + + ```python from typing import List @@ -165,40 +174,40 @@ from supertokens_python.recipe.usermetadata.asyncio import ( ) -async def add_email_to_allow_list(email: str): - metadataResult = await get_user_metadata("emailAllowList") +async def add_email_to_allow_list(tenant_id: str, email: str): + metadata_key = f"{tenant_id}:emailAllowList" + metadataResult = await get_user_metadata(metadata_key) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(email) - await update_user_metadata("emailAllowList", { + await update_user_metadata(metadata_key, { "allowList": allow_list }) -async def is_email_allowed(email: str): - metadataResult = await get_user_metadata("emailAllowList") +async def is_email_allowed(tenant_id: str, email: str): + metadataResult = await get_user_metadata(f"{tenant_id}:emailAllowList") allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return email in allow_list -async def add_phone_number_to_allow_list(phone_number: str): - metadataResult = await get_user_metadata("phoneNumberAllowList") +async def add_phone_number_to_allow_list(tenant_id: str, phone_number: str): + metadata_key = f"{tenant_id}:phoneNumberAllowList" + metadataResult = await get_user_metadata(metadata_key) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(phone_number) - await update_user_metadata("phoneNumberAllowList", { + await update_user_metadata(metadata_key, { "allowList": allow_list }) -async def is_phone_number_allowed(phone_number: str): - metadataResult = await get_user_metadata("phoneNumberAllowList") +async def is_phone_number_allowed(tenant_id: str, phone_number: str): + metadataResult = await get_user_metadata(f"{tenant_id}:phoneNumberAllowList") allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return phone_number in allow_list ``` - - - + + - -:::info Multi Tenancy -For a multi tenant setup, you can even store an allow list per tenant. This would allow you to limit sign ups for different emails / phone numbers for different tenants. If you are doing this, then you would also need to pass in the tenantID to the functions above, which you can obtain from the input to the `API overrides` shown below. +:::info[Multi Tenancy] +The helpers separate prototype metadata by `tenantId`, which the API overrides provide. User Metadata has no `tenantId` argument, so never use one shared synthetic key across tenants. For production, enforce tenant isolation and atomic updates in application storage. ::: @@ -209,62 +218,65 @@ Update the backend SDK API function to only allow sign up requests from users th To do this you need to use the check functions from the previous code snippet. - - - -```tsx + + +```tsx check=false reason="This example uses allow-list helpers defined in the preceding application code." import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; -declare let isEmailAllowed: (email: string) => Promise // typecheck-only, removed from output -declare let isPhoneNumberAllowed: (email: string) => Promise // typecheck-only, removed from output Passwordless.init({ - contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - createCodePOST: async function (input) { - if ("email" in input) { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - email: input.email, - }); - let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); - if (userWithPasswordles === undefined) { - // this is sign up attempt - if (!(await isEmailAllowed(input.email))) { - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } - } - } - } else { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - phoneNumber: input.phoneNumber, - }); - let userWithPasswordles = existingUsers.find(u => u.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); - if (userWithPasswordles === undefined) { - // this is sign up attempt - if (!(await isPhoneNumberAllowed(input.phoneNumber))) { - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } - } - } - } - return await originalImplementation.createCodePOST!(input); - } + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + createCodePOST: async function (input) { + if ("email" in input) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email, + }); + let userWithPasswordles = existingUsers.find( + (u) => + u.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== + undefined, + ); + if (userWithPasswordles === undefined) { + // this is sign up attempt + if (!(await isEmailAllowed(input.tenantId, input.email))) { + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin.", + }; + } + } + } else { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + phoneNumber: input.phoneNumber, + }); + let userWithPasswordles = existingUsers.find( + (u) => + u.loginMethods.find( + (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless", + ) !== undefined, + ); + if (userWithPasswordles === undefined) { + // this is sign up attempt + if (!(await isPhoneNumberAllowed(input.tenantId, input.phoneNumber))) { + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin.", + }; + } } - } - } -}) + } + return await originalImplementation.createCodePOST!(input); + }, + }; + }, + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -272,12 +284,12 @@ import ( "github.com/supertokens/supertokens-golang/supertokens" ) -func isEmailAllowed(email string) (bool, error) { +func isEmailAllowed(tenantId, email string) (bool, error) { // ... from previous code snippet return false, nil } -func isPhoneNumberAllowed(phoneNumber string) (bool, error) { +func isPhoneNumberAllowed(tenantId, phoneNumber string) (bool, error) { // ... from previous code snippet return false, nil } @@ -297,7 +309,7 @@ func main() { } if existingUser == nil { // sign up attempt - emailAllowed, err := isEmailAllowed(*email) + emailAllowed, err := isEmailAllowed(tenantId, *email) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } @@ -316,7 +328,7 @@ func main() { } if existingUser == nil { // sign up attempt - phoneNumberAllowed, err := isPhoneNumberAllowed(*phoneNumber) + phoneNumberAllowed, err := isPhoneNumberAllowed(tenantId, *phoneNumber) if err != nil { return plessmodels.CreateCodePOSTResponse{}, err } @@ -338,11 +350,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init @@ -357,12 +367,12 @@ from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput -async def is_email_allowed(email: str): +async def is_email_allowed(tenant_id: str, email: str): # from previous code snippet.. return False -async def is_phone_number_allowed(phone_number: str): +async def is_phone_number_allowed(tenant_id: str, phone_number: str): # from previous code snippet.. return False @@ -398,7 +408,7 @@ def override_passwordless_apis(original_implementation: APIInterface): if user_with_passwordless is None: # sign up attempt - if not (await is_email_allowed(email)): + if not (await is_email_allowed(tenant_id, email)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) @@ -422,7 +432,7 @@ def override_passwordless_apis(original_implementation: APIInterface): if user_with_passwordless is None: # sign up attempt - if not (await is_phone_number_allowed(phone_number)): + if not (await is_phone_number_allowed(tenant_id, phone_number)): return GeneralErrorResponse( "Sign ups disabled. Please contact admin." ) @@ -443,10 +453,9 @@ def override_passwordless_apis(original_implementation: APIInterface): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ passwordless.init( - contact_config="", # type: ignore # typecheck-only, removed from output flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, @@ -455,19 +464,18 @@ init( ], ) ``` - - - + + --- ## See also - - - - - - - + + + + + + + diff --git a/docs/authentication/passwordless/configure-email-and-sms-behavior.mdx b/docs/authentication/passwordless/configure-email-and-sms-behavior.mdx index 0a6ea9d899..22f0d8434e 100644 --- a/docs/authentication/passwordless/configure-email-and-sms-behavior.mdx +++ b/docs/authentication/passwordless/configure-email-and-sms-behavior.mdx @@ -1,101 +1,82 @@ --- title: Configure email and SMS behavior -hide_title: true -sidebar_position: 7 description: Customize how the email and SMS are sent in your frontend application. -page_type: guide -recipe: passwordless -category: passwordless +sidebar: + order: 60 --- - -# Configure email and SMS behavior - - - - ## Changing email / SMS resend time interval -:::caution no-title +:::warning These instructions are only applicable if you are using the pre-built UI. ::: - You can set `resendEmailOrSMSGapInSeconds` to establish a minimum delay before the frontend allows the user to click the "Resend" button. -This limit is only enforced on the client-side. For API rate-limiting please check out the [deployment section](/docs/deployment/rate-limits). - +This limit is only enforced on the client-side. For API rate-limiting please check out the [deployment section](/deployment/rate-limits). - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. - // highlight-start - signInUpFeature: { - // The default value is 15 seconds - resendEmailOrSMSGapInSeconds: 60, - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. + signInUpFeature: { + // The default value is 15 seconds + resendEmailOrSMSGapInSeconds: 60, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. - // highlight-start - signInUpFeature: { - // The default value is 15 seconds - resendEmailOrSMSGapInSeconds: 60, - } - // highlight-end - }), - supertokensUISession.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", // This example will work with any contactMethod. + signInUpFeature: { + // The default value is 15 seconds + resendEmailOrSMSGapInSeconds: 60, + }, + }), + supertokensUISession.init({ + /* ... */ + }), + ], }); ``` - - - - + + --- ## Setting default country for phone inputs -:::caution no-title +:::warning These instructions are only applicable if you are using the pre-built UI. ::: @@ -117,72 +98,64 @@ By default, there is no default country selected. This means that users have to If you would like to set a default country (for all users), then you should use the `defaultCountry` configuration: - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ contactMethod: "PHONE", - // highlight-start - signInUpFeature: { - /* - * Must be a two-letter ISO country code (e.g.: "US") - */ - defaultCountry: "HU", - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + signInUpFeature: { + /* + * Must be a two-letter ISO country code (e.g.: "US") + */ + defaultCountry: "HU", + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIPasswordless.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIPasswordless.init({ contactMethod: "PHONE", - // highlight-start - signInUpFeature: { - /* - * Must be a two-letter ISO country code (e.g.: "US") - */ - defaultCountry: "HU", - } - // highlight-end - }), - supertokensUISession.init({ /* ... */ }) - ] + signInUpFeature: { + /* + * Must be a two-letter ISO country code (e.g.: "US") + */ + defaultCountry: "HU", + }, + }), + supertokensUISession.init({ + /* ... */ + }), + ], }); ``` - - - + + - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ contactMethod: "EMAIL_OR_PHONE", - // highlight-start - signInUpFeature: { - /* - * Must be a two-letter ISO country code (e.g.: "US") - */ - defaultCountry: "HU", - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + signInUpFeature: { + /* + * Must be a two-letter ISO country code (e.g.: "US") + */ + defaultCountry: "HU", + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - // highlight-start - signInUpFeature: { - /* - * Must be a two-letter ISO country code (e.g.: "US") - */ - defaultCountry: "HU", - } - // highlight-end - }), - supertokensUISession.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", + signInUpFeature: { + /* + * Must be a two-letter ISO country code (e.g.: "US") + */ + defaultCountry: "HU", + }, + }), + supertokensUISession.init({ + /* ... */ + }), + ], }); ``` - - - + + @@ -268,13 +233,11 @@ supertokensUIInit({ ## See also - - - - - - - - - - + + + + + + + + diff --git a/docs/authentication/passwordless/customize-the-magic-link.mdx b/docs/authentication/passwordless/customize-the-magic-link.mdx index ab75b47f7f..6be42a4d2b 100644 --- a/docs/authentication/passwordless/customize-the-magic-link.mdx +++ b/docs/authentication/passwordless/customize-the-magic-link.mdx @@ -1,69 +1,61 @@ --- title: Customize the Magic Link -hide_title: true -sidebar_position: 3 description: See how to change the magic link or how to generate it manually -page_type: guide -recipe: passwordless -category: passwordless +sidebar: + order: 30 --- -# Customize the magic link - ## Change the magic link URL ### Override the email delivery backend function You can change the URL of Magic Links by providing overriding the email delivery configuration on the backend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod - // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "MAGIC_LINK" flows. - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - - // highlight-start - emailDelivery: { - // highlight-start - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - return originalImplementation.sendEmail({ - ...input, - urlWithLinkCode: input.urlWithLinkCode?.replace( - // This is: `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/verify` - "http://localhost:3000/auth/verify", - "http://your.domain.com/your/path" - ) - }) - } - } - } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", // This example will work with any contactMethod + // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "MAGIC_LINK" flows. + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + return originalImplementation.sendEmail({ + ...input, + urlWithLinkCode: input.urlWithLinkCode?.replace( + // This is: `/auth/verify` + "http://localhost:3000/auth/verify", + "http://your.domain.com/your/path", + ), + }); + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "strings" @@ -79,11 +71,10 @@ func main() { RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ EmailDelivery: &emaildelivery.TypeInput{ - // highlight-start Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { ogSendEmail := *originalImplementation.SendEmail (*originalImplementation.SendEmail) = func(input emaildelivery.EmailType, userContext supertokens.UserContext) error { - // By default: `^{appInfo.websiteDomain}/^{appInfo.websiteBasePath}/verify` + // By default: `//auth/verify` newUrl := strings.Replace( *input.PasswordlessLogin.UrlWithLinkCode, "http://localhost:3000/auth/verify", @@ -95,18 +86,15 @@ func main() { } return originalImplementation }, - // highlight-end }, }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import passwordless @@ -116,97 +104,100 @@ from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConf def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> EmailDeliveryOverrideInput: original_send_email = original_implementation.send_email - # highlight-start async def send_email(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> None: assert template_vars.url_with_link_code is not None - # By default: `^{appInfo.websiteDomain}/^{appInfo.websiteBasePath}/verify` + # By default: `//auth/verify` template_vars.url_with_link_code = template_vars.url_with_link_code.replace( "http://localhost:3000/auth/verify", "http://localhost:3000/custom/path") return await original_send_email(template_vars, user_context) - # highlight-end original_implementation.send_email = send_email return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ) ] ) ``` - - - + + ### Change the frontend page - - - + - - + -When the user clicks the magic link, you need to render the `LinkClicked` component that exported by the SDK on that page. By default, this already happens on the `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/verify` path. To change this, you need to: + + +When the user clicks the magic link, you need to render the `LinkClicked` component that exported by the SDK on that page. By default, this already happens on the `/auth/verify` path. To change this, you need to: #### 1. Disable the default UI for the link clicked screen: + + +When the user clicks the magic link, you need to build your own UI on that page to handle the link clicked. You also need to disable the pre-built UI provided by the SDK for the link clicked screen as shown below: + + + + + ```tsx import Passwordless from "supertokens-auth-react/recipe/passwordless"; Passwordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod - linkClickedScreenFeature: { - disableDefaultUI: true - }, + contactMethod: "EMAIL", // This example will work with any contactMethod + linkClickedScreenFeature: { + disableDefaultUI: true, + }, }); ``` - -#### 2. Render the link clicked screen on your custom route: -```tsx -import React from "react"; -import { LinkClicked } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; -function CustomLinkClickedScreen () { - return -} -``` - - - - - -When the user clicks the magic link, you need to build your own UI on that page to handle the link clicked. You also need to disable the pre-built UI provided by the SDK for the link clicked screen as shown below: - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; supertokensUIPasswordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod - linkClickedScreenFeature: { - disableDefaultUI: true - }, + contactMethod: "EMAIL", // This example will work with any contactMethod + linkClickedScreenFeature: { + disableDefaultUI: true, + }, }); ``` + + - + + +#### 2. Render the link clicked screen on your custom route: + + - + + +```tsx +import React from "react"; +import { LinkClicked } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; +function CustomLinkClickedScreen() { + return ; +} +``` + + - - + + -:::info Caution +:::info[Caution] Not applicable since you do not use the pre-built UI ::: - + --- @@ -215,22 +206,19 @@ Not applicable since you do not use the pre-built UI You can use the backend SDK to generate magic links as shown below: - - - + + ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; async function createMagicLink(email: string) { - const magicLink = await Passwordless.createMagicLink({email, tenantId: "public"}); + const magicLink = await Passwordless.createMagicLink({ email, tenantId: "public" }); - console.log(magicLink); + console.log(magicLink); } ``` - - - - + + ```go import ( "fmt" @@ -250,15 +238,10 @@ func main() { fmt.Println(magicLink) } ``` - - - - - - - - - + + + + ```python from supertokens_python.recipe.passwordless.asyncio import create_magic_link @@ -267,10 +250,8 @@ async def create_link(email: str): print(magic_link) ``` - - - - + + ```python from supertokens_python.recipe.passwordless.syncio import create_magic_link @@ -279,15 +260,12 @@ def create_link(email: str): print(magic_link) ``` + + + + - - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that you pass the `"public"` `tenantId` to the function call above - which is the default `tenantId`. @@ -303,64 +281,51 @@ Note that the generated link uses the configured `websiteDomain` from the `appIn You can change how long a user can use an OTP or a Magic Link to log in by changing the `passwordless_code_lifetime` core configuration value. You configure this value in milliseconds and it defaults to `900000` (15 minutes). -:::caution - -Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime according to the `passwordless_code_lifetime` setting. +:::warning[Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime according to the `passwordless_code_lifetime` setting.] ::: - - + + +- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. +- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. + + + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e PASSWORDLESS_CODE_LIFETIME=60000 \ - // highlight-end -d supertokens/supertokens- ``` - - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_code_lifetime: 60000 - ``` - - - - -- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. -- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. - + + ```yaml passwordless_code_lifetime: 60000 ``` - - + + --- ## See also - - - - - - - - - + + + + + + + + + diff --git a/docs/authentication/passwordless/customize-the-otp.mdx b/docs/authentication/passwordless/customize-the-otp.mdx index f425ef5ba4..f8036b6740 100644 --- a/docs/authentication/passwordless/customize-the-otp.mdx +++ b/docs/authentication/passwordless/customize-the-otp.mdx @@ -1,53 +1,42 @@ --- title: Customize the One-Time Password (OTP) -hide_title: true -sidebar_position: 3 description: Configure OTP by changing the format or by modifying the token duration -page_type: guide -recipe: passwordless -category: passwordless +sidebar: + order: 40 --- - -# Customize the one-time password - - ## Change the OTP format By default, the generated OTP is 6 digits long and is numbers only. You can change this to be any length you like and have any character set by providing the `getCustomUserInputCode` function. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod - // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "USER_INPUT_CODE" flows. - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - - // highlight-start - getCustomUserInputCode: async (userCtx) => { - // TODO: - return "123abcd"; - }, - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", // This example will work with any contactMethod + // This example works with the "USER_INPUT_CODE_AND_MAGIC_LINK" and "USER_INPUT_CODE" flows. + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + + getCustomUserInputCode: async (userCtx) => { + // TODO: + return "123abcd"; + }, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -68,11 +57,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless from typing import Dict, Any @@ -82,21 +69,18 @@ async def get_custom_user_input_code(tenant_id: str, user_context: Dict[str, Any init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config=..., # type: ignore - flow_type="...", # type: ignore - # highlight-start + contact_config=..., + flow_type="...", get_custom_user_input_code=get_custom_user_input_code - # highlight-end ) ] ) ``` - - - + + --- @@ -104,48 +88,36 @@ init( You can change how many times a user can attempt to enter an OTP before they have to enter their email / phone number again (thereby force generating a new OTP). By default, this value is `5` attempts, and you can modify it by changing the `passwordless_max_code_input_attempts` core configuration: - - - + + +- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. +- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. + + + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS=3 \ - // highlight-end -d supertokens/supertokens- ``` - - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_max_code_input_attempts: 3 - ``` - - - - -- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. -- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. - + + ```yaml passwordless_max_code_input_attempts: 3 ``` - - + + --- @@ -154,64 +126,51 @@ passwordless_max_code_input_attempts: 3 You can change how long a user can use an OTP or a Magic Link to log in by changing the `passwordless_code_lifetime` core configuration value. This value defaults to `900000` milliseconds (15 minutes). -:::caution - -Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime per the `passwordless_code_lifetime` setting. +:::warning[Each new OTP / magic link generated, either by opening a new browser or by clicking on the "Resend" button, has a lifetime per the `passwordless_code_lifetime` setting.] ::: - - + + +- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. +- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. + + + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e PASSWORDLESS_CODE_LIFETIME=60000 \ - // highlight-end -d supertokens/supertokens- ``` - - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command passwordless_code_lifetime: 60000 - ``` - - - - -- Open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and open **Configuration**. -- In the **Passwordless** configuration card, change the value. Configuration changes are saved automatically. - + + ```yaml passwordless_code_lifetime: 60000 ``` - - + + --- ## See also - - - - - - - - - + + + + + + + + + diff --git a/docs/authentication/passwordless/hooks-and-overrides.mdx b/docs/authentication/passwordless/hooks-and-overrides.mdx index 9180c3afdb..5f15d2cfb2 100644 --- a/docs/authentication/passwordless/hooks-and-overrides.mdx +++ b/docs/authentication/passwordless/hooks-and-overrides.mdx @@ -1,197 +1,168 @@ --- title: Hooks and overrides -hide_title: true -sidebar_position: 5 -description: >- - Add custom logic in the authentication flow by overriding the SuperTokens - APIs. -page_type: guide -recipe: passwordless -category: passwordless +description: Add custom logic in the authentication flow by overriding the SuperTokens APIs. +sidebar: + order: 50 --- - -# Hooks and overrides - - -**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. +**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `passwordless` recipe to your needs. -Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. +Explore the [references pages](/references) for a more in depth guide on hooks and overrides. -## Frontend hook +## Frontend hook - + - + -This method gets fired, after certain events in the `passwordles` authentication flow. +This method gets fired, after certain events in the `passwordles` authentication flow. Use it to fire different types of events immediately and introduce custom logic based on your use case. - - - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import Passwordless from "supertokens-auth-react/recipe/passwordless"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL_OR_PHONE", - - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "PASSWORDLESS_RESTART_FLOW") { - // TODO: - } else if (context.action === "PASSWORDLESS_CODE_SENT") { - // TODO: - } else { - let {id, emails, phoneNumbers} = context.user; - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + + onHandleEvent: async (context) => { + if (context.action === "PASSWORDLESS_RESTART_FLOW") { + // TODO: + } else if (context.action === "PASSWORDLESS_CODE_SENT") { + // TODO: + } else { + let { id, emails, phoneNumbers } = context.user; + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in } - // highlight-end - }), - Session.init() - ] + } + } + }, + }), + Session.init(), + ], }); ``` - - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIPasswordless.init({ - contactMethod: "EMAIL_OR_PHONE", - - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "PASSWORDLESS_RESTART_FLOW") { - // TODO: - } else if (context.action === "PASSWORDLESS_CODE_SENT") { - // TODO: - } else { - let {id, emails, phoneNumbers} = context.user; - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIPasswordless.init({ + contactMethod: "EMAIL_OR_PHONE", + + onHandleEvent: async (context) => { + if (context.action === "PASSWORDLESS_RESTART_FLOW") { + // TODO: + } else if (context.action === "PASSWORDLESS_CODE_SENT") { + // TODO: + } else { + let { id, emails, phoneNumbers } = context.user; + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in } - // highlight-end - }), - supertokensUISession.init() - ] + } + } + }, + }), + supertokensUISession.init(), + ], }); ``` + + + - - - - - + - - -:::caution Not applicable +:::warning[Not applicable] This section is not applicable for custom UI since you are calling the consume code API yourself anyway. You can perform any actions post sign in / up based on the result of the API call. ::: - + ## Backend override Overriding the `consumeCode` function allows you to introduce your own logic for the authentication process. Use it to persist different types of data or trigger actions. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - contactMethod: "EMAIL", // This example will work with any contactMethod - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // This example will work with any flowType - - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - consumeCode: async (input) => { - - // First we call the original implementation of consumeCode. - let response = await originalImplementation.consumeCode(input); - - // Post sign up response, we check if it was successful - if (response.status === "OK") { - let { id, emails, phoneNumbers } = response.user; - - if (input.session === undefined) { - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: post sign up logic - } else { - // TODO: post sign in logic - } - } - } - return response; - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", // This example will work with any contactMethod + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // This example will work with any flowType + + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + consumeCode: async (input) => { + // First we call the original implementation of consumeCode. + let response = await originalImplementation.consumeCode(input); + + // Post sign up response, we check if it was successful + if (response.status === "OK") { + let { id, emails, phoneNumbers } = response.user; + + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: post sign up logic + } else { + // TODO: post sign in logic + } } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + } + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "fmt" @@ -206,7 +177,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start Override: &plessmodels.OverrideStruct{ Functions: func(originalImplementation plessmodels.RecipeInterface) plessmodels.RecipeInterface { // create a copy of the original function @@ -241,17 +211,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session, passwordless from supertokens_python.recipe.passwordless.interfaces import ( @@ -262,7 +229,6 @@ from typing import Dict, Any, Union, Optional from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_passwordless_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: @@ -310,43 +276,38 @@ def override_passwordless_functions( return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ passwordless.init( contact_config=passwordless.ContactConfig( contact_method="EMAIL", # This example will work with any contactMethod ), flow_type="USER_INPUT_CODE_AND_MAGIC_LINK", # This example will work with any flowType - # highlight-start override=passwordless.InputOverrideConfig( functions=override_passwordless_functions ), - # highlight-end ), session.init(), ], ) ``` - - - + + --- ## See also - - - - - - - - - - + + + + + + + + + + diff --git a/docs/authentication/passwordless/initial-setup.mdx b/docs/authentication/passwordless/initial-setup.mdx index 64d6e890c2..15cb457002 100644 --- a/docs/authentication/passwordless/initial-setup.mdx +++ b/docs/authentication/passwordless/initial-setup.mdx @@ -1,555 +1,1716 @@ --- -title: Initial Setup -hide_title: true -sidebar_position: 2 -description: >- - Add Passwordless authentication to your project using Prebuilt or Custom UI - components. -page_type: tutorial -recipe: passwordless -category: passwordless +title: Set Up Passwordless Authentication +description: Integrate email or SMS passwordless authentication with magic links, OTPs, or both using prebuilt or custom UI. +sidebar: + label: Initial Setup + order: 20 --- -import ReactRouterCallout from "/docs/_blocks/react-router-callout.mdx"; -import FrontendCustomUIMagicLinkUI from "./_blocks/frontend-custom-ui-magic-link-ui.mdx" -import FrontendCustomUIOtpUI from "./_blocks/frontend-custom-ui-otp-ui.mdx" -import BackendSDKInit from "./_blocks/backend-sdk-init.mdx" + -# Initial setup +## Passwordless integration summary -## Overview +- Configure the Passwordless and Session recipes on both the frontend and backend, then add the selected UI and authentication routes. +- Set `contactMethod` to email, phone, or email-or-phone delivery. +- Set `flowType` to `MAGIC_LINK`, `USER_INPUT_CODE` for OTPs, or `USER_INPUT_CODE_AND_MAGIC_LINK` for both. +- Configure email or SMS delivery. Test sign-in, resend, expired or invalid credentials, and session creation. -This page shows you how to add the **Passwordless** `recipe` to your project. -The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. + -## Before you start - -:::important + +Implement SuperTokens passwordless authentication in this application. Inspect the existing frontend, backend, recipes, and routing first. Ask whether users should authenticate through email, SMS, or both, and whether the flow should use magic links, OTPs, or both. Configure the frontend and backend Passwordless and Session recipes, the selected UI, auth routes, and email or SMS delivery. Preserve existing conventions, keep credentials in environment variables, and validate sign-in, resend, expiry, and session behavior. + -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page please follow the tutorial and return here once you're done. +## Overview -::: +This page shows you how to add the **Passwordless** `recipe` to your project. +The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ### Terminology Before going into the actual steps lets first talk about two terms that influence how you configure the **Passwordless** recipe. - **Contact Method**: This defines how the user receives the credentials from your app. You can choose between `email`, `phone number` or both (the user has to choose one during the login flow). -- **Flow Type**: This is the actual credential type used for authentication. You can choose between **Magic Link**, **OTP** (One-Time Password), or both (the user has to choose one during the login flow). +- **Flow Type**: This is the credential type used for authentication. You can choose **Magic Link**, **OTP** (One-Time Password), or both. The combined flow sends both credentials and the user can complete authentication with either one. ## Steps - + - + ### 1. Initialize the frontend SDK - - - + + #### 1.1 Add the `Passwordless` recipe in your main configuration file. - + + +Add the `Passwordless` recipe in your `AuthComponent`. + + +Add the `Passwordless` recipe in your `AuthView` file. + + + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -// highlight-start import Passwordless from "supertokens-auth-react/recipe/passwordless"; -// highlight-end import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ -// highlight-start - Passwordless.init({ - contactMethod: "^{recipes.passwordless.contactMethod}" - }), -// highlight-end - Session.init() - ] + appInfo: { + // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + contactMethod: "EMAIL", + }), + Session.init(), + ], }); ``` + + +```tsx title="/app/auth/auth.component.ts" check=false reason="This example omits surrounding application and SuperTokens configuration." +import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; +import { DOCUMENT } from "@angular/common"; + +@Component({ + selector: "app-auth", + template: '
', +}) +export class AuthComponent implements OnDestroy, AfterViewInit { + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + ) {} + + ngAfterViewInit() { + this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); + } + + ngOnDestroy() { + // Remove the script when the component is destroyed + const script = this.document.getElementById("supertokens-script"); + if (script) { + script.remove(); + } + } + + private loadScript(src: string) { + const script = this.renderer.createElement("script"); + script.type = "text/javascript"; + script.src = src; + script.id = "supertokens-script"; + script.onload = () => { + supertokensUIInit({ + appInfo: { + // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + supertokensUIPasswordless.init({ + contactMethod: "EMAIL", + }), + supertokensUISession.init(), + ], + }); + }; + this.renderer.appendChild(this.document.body, script); + } +} +``` +
+ +```html + + +``` + +
- - + + #### 1.2 Include the pre-built UI components in your application. To render the **Pre-Built UI** inside your application, you need to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. + + - - + + + + ```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; - -// highlight-next-line -import { PasswordlessPreBuiltUI } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +import React from "react"; +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; + +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the ^{appInfo.websiteBasePath} route*/} - // highlight-next-line - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } } ``` - - - - - - + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -import { PasswordlessPreBuiltUI } from 'supertokens-auth-react/recipe/passwordless/prebuiltui'; +import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { - render() { - // highlight-start - if (canHandleRoute([PasswordlessPreBuiltUI])) { - // This renders the login UI on the ^{appInfo.websiteBasePath} route - return getRoutingComponent([PasswordlessPreBuiltUI]) - } - // highlight-end - - return ( - {/*Your app*/} - ); + render() { + if (canHandleRoute([PasswordlessPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([PasswordlessPreBuiltUI]); } + return {/*Your app*/}; + } } ``` - - - -
- - - - -Add the `Passwordless` recipe in your `AuthComponent`. - - - ```tsx title="/app/auth/auth.component.ts" - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document - ) { } - - ngAfterViewInit() { - this.loadScript('^{prebuiltUIVersion}'); - } + + + + + + + + + +:::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] +Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. + + + + + + + + + +```tsx +import React from "react"; - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById('supertokens-script'); - if (script) { - script.remove(); - } - } +import { BrowserRouter, useRoutes } from "react-router-dom"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import * as reactRouterDom from "react-router-dom"; - private loadScript(src: string) { - const script = this.renderer.createElement('script'); - script.type = 'text/javascript'; - script.src = src; - script.id = 'supertokens-script'; - script.onload = () => { - supertokensUIInit({ - appInfo: { - // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ -// highlight-start - supertokensUIPasswordless.init({ - contactMethod: "^{recipes.passwordless.contactMethod}" - }), -// highlight-end - supertokensUISession.init(), - ], - }); - } - this.renderer.appendChild(this.document.body, script); - } - } - ``` +function AppRoutes() { + const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ + ]); -
+ const routes = useRoutes([ + ...authRoutes.map((route) => route.props), + // Include the rest of your app routes + ]); -
- + return routes; +} - +function App() { + return ( + + + + + + ); +} +``` + + + + + + + + + +::: + + + + - -Add the `Passwordless` recipe in your `AuthView` file. - - - ```tsx - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIPasswordless from "supertokens-auth-react-script/recipe/passwordless"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - - - ``` +### 2. Initialize the backend SDK - +You need to initialize the **Backend SDK** alongside the code that starts your server. +The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. - +For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. +Click one of the options from the next form and the code snippet updates. -
+ + +```tsx title="Backend SDK Init" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import Passwordless from "supertokens-node/recipe/passwordless"; +supertokens.init({ + // Replace this with the framework you are using + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "MAGIC_LINK", + contactMethod: "EMAIL", + }), + Session.init(), + ], +}); +``` + + +```python title="Backend SDK Init" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import passwordless, session +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + passwordless.init( + flow_type="MAGIC_LINK", + contact_config=ContactEmailOnlyConfig() + ) + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + + +```go title="Backend SDK Init" +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/passwordless" + "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + passwordless.Init(plessmodels.TypeInput{ + FlowType: "MAGIC_LINK", + ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, + }), + session.Init(nil), // initializes session features + }, + }) -### 2. Initialize the backend SDK + if err != nil { + panic(err.Error()) + } +} +``` + + - +
-
+ - ### 1. Initialize the frontend SDK Call the SDK init function at the start of your application. -The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. - - - +The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. - - + + + + +First, you need to add the recipe script tag. + + + + + + +Add the `SuperTokens.init` function call at the start of your application. + + + + + + + + + +```tsx +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import Passwordless from "supertokens-web-js/recipe/passwordless"; +SuperTokens.init({ + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [Session.init(), Passwordless.init()], +}); +``` + + +```html + +``` + + + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import Passwordless from 'supertokens-web-js/recipe/passwordless' +import SuperTokens from "supertokens-react-native"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - Session.init(), - Passwordless.init(), - ], + apiDomain: "", + apiBasePath: "/auth", }); ``` + + +```kotlin +import android.app.Application +import com.supertokens.session.SuperTokens +class MainApplication: Application() { + override fun onCreate() { + super.onCreate() - + SuperTokens.Builder(this, "") + .apiBasePath("/auth") + .build() + } +} +``` + + +```swift +import UIKit +import SuperTokensIOS - +fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + try SuperTokens.initialize( + apiDomain: "", + apiBasePath: "/auth" + ) + } catch SuperTokensError.initError(let message) { + // TODO: Handle initialization error + } catch { + // Some other error + } -First, you need to add the recipe script tag. + return true + } -```html - +} ``` + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; +void main() { + SuperTokens.init( + apiDomain: "", + apiBasePath: "/auth", + ); +} +``` + + + + + + + + + You can initialize the SDK + + + + + + + + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +supertokens.init({ + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [supertokensSession.init(), supertokensPasswordless.init()], +}); +``` + + + + + + - +### 2. Add the login UI + +Follow the section that matches your configured `flowType`. For `USER_INPUT_CODE_AND_MAGIC_LINK`, one create-code request sends both a magic link and an OTP. Use the shared create and resend behavior from steps 2.1 and 2.2, then implement both consumption paths so the user can complete either one. + +#### Magic Link + +The following section shows you what aspects you need to cover to implement the UI for a `Magic Link` flow. +The same flow applies during either sign up or sign in. +This guide shows you how to determine if the system creates a new user in the next steps. + +##### 2.1 Sending the Magic link + +You need to add a form that asks the user for their email address or phone number. +When the user submits the form, you need to call the following API to create and send them a **Magic Link**. + +:::info[You configure the contact method on the next page, where you discuss the process of adding the `SDK` to your backend app.] + +::: + + + + + + +For email based login + + + + + + + ```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensPasswordless from 'supertokens-web-js-script/recipe/passwordless' -supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - supertokensPasswordless.init(), - ], -}); +import { createCode } from "supertokens-web-js/recipe/passwordless"; + +async function sendMagicLink(email: string) { + try { + let response = await createCode({ + email, + }); + /** + * For phone number, use this: + + let response = await createCode({ + phoneNumber: "+1234567890" + }); + + */ + + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function sendMagicLink(email: string) { + try { + let response = await supertokensPasswordless.createCode({ + email, + }); + /** + * For phone number, use this: + + let response = await supertokensPasswordless.createCode({ + phoneNumber: "+1234567890" + }); + + */ + + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // Magic link sent successfully. + window.alert("Please check your email for the magic link"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```bash +curl --location --request POST '/auth/public/signinup/code' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "email": "johndoe@gmail.com" +}' +``` + + + + + +For phone number based login + + + + + +```bash +curl --location --request POST '/auth/public/signinup/code' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "phoneNumber": "+1234567890" +}' ``` + + + + + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: This means that the magic link was successfully sent. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +The response from the API call is the following object (in case of `status: "OK"`): + + + + + +```typescript check=false reason="This block documents the response shape rather than executable code." +{ + status: "OK"; + deviceId: string; + preAuthSessionId: string; + flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; +} +``` + + - + + +You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - +- Resend a new magic link. +- Detect if the user has already sent a magic link before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the resend link button. + + - - - +##### 2.2 Resending a magic link - +After sending the initial magic link to the user, you may want to display a resend button to them. +When the user clicks on this button, you should call the following API + + + + +```tsx +import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; + +async function resendMagicLink() { + try { + let response = await resendCode(); + + if (response.status === "RESTART_FLOW_ERROR") { + // this can happen if the user has already successfully logged in into + // another device whilst also trying to login to this one. + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } else { + // Magic link resent successfully. + window.alert("Please check your email for the magic link"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function resendMagicLink() { + try { + let response = await supertokensPasswordless.resendCode(); + + if (response.status === "RESTART_FLOW_ERROR") { + // this can happen if the user has already successfully logged in into + // another device whilst also trying to login to this one. + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await supertokensPasswordless.clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } else { + // Magic link resent successfully. + window.alert("Please check your email for the magic link"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```bash +curl --location --request POST '/auth/public/signinup/code/resend' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "deviceId": "...", + "preAuthSessionId": "...." +}' +``` + + + + + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: This means that the magic link was successfully sent. +- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. + + + + + +##### How to detect if the initial OTP has been sent + +If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. +To prevent this you need a way to know which UI to show. + + + +Since you save the `preAuthSessionId` and `deviceId` after sending the initial magic link, you can know if the user is on either **step 2.1** or **step 2.2**. Check if these tokens are on the device. + +If they aren't, you should follow **step 2.1**, else follow **step 2.2**. + +:::note[You need to clear these tokens if:] + +- the user navigates away from the **step 2.2** page +- you get a `RESTART_FLOW_ERROR` at any point in time from an API call +- the user has successfully logged in. +::: + + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; -SuperTokens.init({ - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", -}); +async function hasInitialMagicLinkBeenSent() { + return (await getLoginAttemptInfo()) !== undefined; +} ``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function hasInitialMagicLinkBeenSent() { + return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; +} +``` + + + + + + - + + +If `hasInitialMagicLinkBeenSent` returns `true`, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number. + + - -Add the `SuperTokens.init` function call at the start of your application. +##### 2.3 Consuming the magic link -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens +When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow. +To do this you ca use this code sample. -class MainApplication: Application() { - override fun onCreate() { - super.onCreate() - - SuperTokens.Builder(this, "^{appInfo.apiDomain}") - .apiBasePath("^{appInfo.apiBasePath}") - .build() + + + +Since you save the `preAuthSessionId` and `deviceId`, you can check if they exist on the app. If they do, then it's the same device that the user has opened the link on, else it's a different device. + + + + + + + +```tsx +import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; + +async function isThisSameBrowserAndDevice() { + return (await getLoginAttemptInfo()) !== undefined; +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function isThisSameBrowserAndDevice() { + return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; +} +``` + + + + + + + + + + +:::note[Add a intermediate step if the user came from a different device.] + +::: + +If the user clicked on a link from a different device, you need to show some kind of an intermediate UI. +This is to protect against email clients opening the magic link on their servers and consuming the link. + +The page should require additional user interaction before consuming the magic link. +For example, you could show a button with the following text: `Click here to login into this device`. +On click, you can consume the magic link to log the user into that device. + +With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link. + + + + +You need to remove the `linkCode` and `preAuthSessionId` from the Magic link. For example, if the Magic link is + + + + + + + +```tsx +import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; + +async function handleMagicLinkClicked() { + try { + let response = await consumeCode(); + + if (response.status === "OK") { + // we clear the login attempt info that was added when the createCode function + // was called since the login was successful. + await clearLoginAttemptInfo(); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // user sign up success + } else { + // user sign in success + } + window.location.assign("/home"); + } else { + // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } } ``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function handleMagicLinkClicked() { + try { + let response = await supertokensPasswordless.consumeCode(); + + if (response.status === "OK") { + // we clear the login attempt info that was added when the createCode function + // was called since the login was successful. + await supertokensPasswordless.clearLoginAttemptInfo(); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // user sign up success + } else { + // user sign in success + } + window.location.assign("/home"); + } else { + // this can happen if the magic link has expired or is invalid + // or if it was denied due to security reasons in case of automatic account linking + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await supertokensPasswordless.clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```text +https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs= +``` + + + + + +Then the `preAuthSessionId` is the value of the query parameter `preAuthSessionId` (`PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=` in the example), and the `linkCode` is the part after the `#` (`s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=` in the example). + +We can then use these to call the consume API + + + + + +```bash +curl --location --request POST '/auth//signinup/code/consume' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=", + "preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=" +}' +``` + + + + +:::info[Multi Tenancy] +Use the `tenantId` query parameter from the magic link as ``. If the link has no `tenantId`, use `public`. The create, resend, and OTP-consume endpoints must use the same tenant path; replace `public` in those examples when authenticating another tenant. +::: - +The response body from the API call has a `status` property in it: - +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + + -```swift -import UIKit -import SuperTokensIOS +#### OTP -fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - do { - try SuperTokens.initialize( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}" - ) - } catch SuperTokensError.initError(let message) { - // TODO: Handle initialization error - } catch { - // Some other error - } +The following section shows you what aspects you need to cover to implement the UI for a `OTP`, One-Time Password, flow. +The same flow applies during either sign up or sign in. +This guide shows you how to determine if you create a new user in the next steps. - return true +##### 2.1 Creating and sending the OTP + +You have to add a form that asks the user for their email address or phone number. +When the users submit the form you have to call the following API to create and send them an OTP. + + + + +For email based login + + + + + + + +```tsx +import { createCode } from "supertokens-web-js/recipe/passwordless"; + +async function sendOTP(email: string) { + try { + let response = await createCode({ + email, + }); + /** + * For phone number, use this: + + let response = await createCode({ + phoneNumber: "+1234567890" + }); + + */ + + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); } - + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function sendOTP(email: string) { + try { + let response = await supertokensPasswordless.createCode({ + email, + }); + /** + * For phone number, use this: + + let response = await supertokensPasswordless.createCode({ + phoneNumber: "+1234567890" + }); + + */ + + if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // OTP sent successfully. + window.alert("Please check your email for an OTP"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you, + // or if the input email / phone number is not valid. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```bash +curl --location --request POST '/auth/public/signinup/code' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "email": "johndoe@gmail.com" +}' +``` + + + + + +For phone number based login + + + + + +```bash +curl --location --request POST '/auth/public/signinup/code' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "phoneNumber": "+1234567890" +}' +``` + + + + + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: This means that the OTP was successfully sent. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +The response from the API call is the following object (in case of `status: "OK"`): + + + + + +```typescript check=false reason="This block documents the response shape rather than executable code." +{ + status: "OK"; + deviceId: string; + preAuthSessionId: string; + flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; } ``` + + + + +You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to: - +- Resend a new OTP. +- Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button. +- Verify the user's input OTP. + + - -```dart -import 'package:supertokens_flutter/supertokens.dart'; -void main() { - SuperTokens.init( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - ); +##### 2.2 Resending a OTP + +After you send the OTP to the user, you may want to display a resend button to them. +When the user clicks on this button, you should call the following API + + + + + + +```tsx +import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; + +async function resendOTP() { + try { + let response = await resendCode(); + + if (response.status === "RESTART_FLOW_ERROR") { + // this can happen if the user has already successfully logged in into + // another device whilst also trying to login to this one. + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } else { + // OTP resent successfully. + window.alert("Please check your email for the OTP"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function resendOTP() { + try { + let response = await supertokensPasswordless.resendCode(); + + if (response.status === "RESTART_FLOW_ERROR") { + // this can happen if the user has already successfully logged in into + // another device whilst also trying to login to this one. + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await supertokensPasswordless.clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } else { + // OTP resent successfully. + window.alert("Please check your email for the OTP"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } } ``` + + + + +```bash +curl --location --request POST '/auth/public/signinup/code/resend' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "deviceId": "...", + "preAuthSessionId": "...." +}' +``` + + + + +The response body from the API call has a `status` property in it: - +- `status: "OK"`: This means that the OTP was successfully sent. +- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. + + - - - +##### How to detect if the initial OTP has been sent +If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page. +To prevent this you need a way to know which UI to show. -### 2. Add the login UI - - - + + +Since you save the `preAuthSessionId` and `deviceId` after sending the initial OTP, you can determine which interface to show. +Check if you stored these tokens on the device. + +If they aren't present, show the form from step 2.1. Otherwise, show the OTP form from step 2.3 with the resend action from step 2.2. + +:::note[You need to clear these tokens if:] + +- the user navigates away from the OTP entry page +- you get a `RESTART_FLOW_ERROR` at any point in time from an API call +- the user has successfully logged in. +::: + + + + + + + +```tsx +import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; - +async function hasInitialOTPBeenSent() { + return (await getLoginAttemptInfo()) !== undefined; +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function hasInitialOTPBeenSent() { + return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined; +} +``` + + + + + + + + + + +If `hasInitialOTPBeenSent` returns `true`, show the OTP form from step 2.3 with the resend action from step 2.2. Otherwise, show the form from step 2.1 asking users to enter their email or phone number. + + + + + +##### 2.3 Verifying the OTP - +When the user enters an OTP you have to call the following API to verify it + + + + + + +```tsx +import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless"; + +async function handleOTPInput(otp: string) { + try { + let response = await consumeCode({ + userInputCode: otp, + }); + + if (response.status === "OK") { + // we clear the login attempt info that was added when the createCode function + // was called since the login was successful. + await clearLoginAttemptInfo(); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // user sign up success + } else { + // user sign in success + } + window.location.assign("/home"); + } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { + // the user entered an invalid OTP + window.alert( + "Wrong OTP! Please try again. Number of attempts left: " + + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount), + ); + } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { + // it can come here if the entered OTP was correct, but has expired because + // it was generated too long ago. + window.alert("Old OTP entered. Please regenerate a new one and try again"); + } else { + // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +async function handleOTPInput(otp: string) { + try { + let response = await supertokensPasswordless.consumeCode({ + userInputCode: otp, + }); + + if (response.status === "OK") { + // we clear the login attempt info that was added when the createCode function + // was called since the login was successful. + await supertokensPasswordless.clearLoginAttemptInfo(); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // user sign up success + } else { + // user sign in success + } + window.location.assign("/home"); + } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") { + // the user entered an invalid OTP + window.alert( + "Wrong OTP! Please try again. Number of attempts left: " + + (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount), + ); + } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") { + // it can come here if the entered OTP was correct, but has expired because + // it was generated too long ago. + window.alert("Old OTP entered. Please regenerate a new one and try again"); + } else { + // this can happen if the user tried an incorrect OTP too many times. + // or if it was denied due to security reasons in case of automatic account linking + + // we clear the login attempt info that was added when the createCode function + // was called - so that if the user does a page reload, they will now see the + // enter email / phone UI again. + await supertokensPasswordless.clearLoginAttemptInfo(); + window.alert("Login failed. Please try again"); + window.location.assign("/auth"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```bash +curl --location --request POST '/auth/public/signinup/code/consume' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "deviceId": "...", + "preAuthSessionId": "...", + "userInputCode": "" +}' +``` + + - + + +The response body from the API call has a `status` property in it: - +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "INCORRECT_USER_INPUT_CODE_ERROR"`: The entered OTP is invalid. The response contains information about the maximum number of retries and the number of failed attempts. +- `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again. +- `status: "RESTART_FLOW_ERROR"`: The user entered invalid OTPs too many times and must restart the flow. +- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + + - - -On success, the backend sends back session tokens as part of the response headers, which are automatically handled by the frontend SDK for you. +On success, the backend sends session tokens in the response. Web SDK requests handle them automatically. Native SDKs only do so when the request uses their integrated HTTP client or interceptor; raw requests such as the `curl` examples must be implemented through that integration in the app. ### 3. Initialize the backend SDK - +You need to initialize the **Backend SDK** alongside the code that starts your server. +The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. + +For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`. +Click one of the options from the next form and the code snippet updates. + + + +```tsx title="Backend SDK Init" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import Passwordless from "supertokens-node/recipe/passwordless"; + +supertokens.init({ + // Replace this with the framework you are using + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + Passwordless.init({ + flowType: "MAGIC_LINK", + contactMethod: "EMAIL", + }), + Session.init(), + ], +}); +``` + + +```python title="Backend SDK Init" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import passwordless, session +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + passwordless.init( + flow_type="MAGIC_LINK", + contact_config=ContactEmailOnlyConfig() + ) + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + + +```go title="Backend SDK Init" +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/passwordless" + "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + passwordless.Init(plessmodels.TypeInput{ + FlowType: "MAGIC_LINK", + ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, + }), + session.Init(nil), // initializes session features + }, + }) + + if err != nil { + panic(err.Error()) + } +} +``` + + - + ## Next steps Having completed the main setup, you can explore more advanced topics related to the **Passwordless** recipe. - - - - Customize the Magic Link - - - Change how Magic Links get created. - - - - - OTP Customization - - - Change the format of the generated One-Time Password. - - - - - Hooks and overrides - - - Add custom logic after the logs in or signs up. - - - - - Email Delivery - - - Customize how emails get delivered to your users. - - - - - SMS Delivery - - - Customize how SMS messages get delivered to your users. - - - - + + +Change how Magic Links get created. + + +Change the format of the generated One-Time Password. + + +Add custom logic after the logs in or signs up. + + +Customize how emails get delivered to your users. + + +Customize how SMS messages get delivered to your users. + + diff --git a/docs/authentication/passwordless/introduction.mdx b/docs/authentication/passwordless/introduction.mdx index fcd26bd067..5c1f54c507 100644 --- a/docs/authentication/passwordless/introduction.mdx +++ b/docs/authentication/passwordless/introduction.mdx @@ -1,97 +1,65 @@ --- -title: Introduction -hide_title: true -sidebar_position: 1 -pagination_next: null -pagination_prev: null -skip_llms_txt: true -description: >- - Authenticate users with passwordless methods like Magic Links or One-Time - Passwords. -page_type: overview -recipe: passwordless -category: passwordless +title: Passwordless Authentication +description: Authenticate users with magic links or one-time passwords delivered through email or SMS, using prebuilt or custom UI. +sidebar: + label: Overview + order: 10 --- + -# Introduction +## Passwordless summary + +- The Passwordless recipe authenticates users with generated Magic Links or One-Time Passwords. +- Use the prebuilt UI or implement a custom interface with the SDKs. +- Customize Magic Link creation, OTP format, and email or SMS delivery when needed. + + ## Overview The **Passwordless** `recipe` provides a way of authenticating users through generated credentials like **Magic Links** or **One-Time Passwords**. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. -Sign in form UI for passwordless login + + Sign in form UI for passwordless login + -## Getting started +## Getting started You can either follow setup guide or use the `CLI` tool to generate an example app that shows you how the recipe works. - - - - Quickstart - - - Go through a quick tutorial that shows you how to add the **Passwordless** recipe to your app. - - - - - Example Applications - - - Use the CLI to generate a boilerplate app that you can use as a starting point. - - - - - -## Customization + + +Go through a quick tutorial that shows you how to add the **Passwordless** recipe to your app. + + -To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Customize the Magic Link - - - Change how you create Magic Links. - - - - - One-Time Password (OTP) Customization - - - Change the format of the generated One-Time Password. - - - - - Hooks and Overrides - - - Add custom logic after the logs in or signs up. - - - - - Email Delivery - - - Customize how you deliver emails to your users. - - - - - SMS Delivery - - - Customize how you deliver SMS messages to your users. - - - +## Customization +To adjust the functionality to fit your use case you can explore different sections from the documentation. + + +Change how you create Magic Links. + + +Change the format of the generated One-Time Password. + + +Add custom logic after the logs in or signs up. + + +Customize how you deliver emails to your users. + + +Customize how you deliver SMS messages to your users. + + diff --git a/docs/authentication/passwordless/invite-link-flow.mdx b/docs/authentication/passwordless/invite-link-flow.mdx index 8bc6585420..520d63cb8a 100644 --- a/docs/authentication/passwordless/invite-link-flow.mdx +++ b/docs/authentication/passwordless/invite-link-flow.mdx @@ -1,18 +1,10 @@ --- title: Implement invite link based sign up -hide_title: true -description: >- - Discover how to implement an invite based sign up flow with the passwordless - recipe. -sidebar_position: 10 -page_type: guide -recipe: passwordless -category: passwordless +description: Discover how to implement an invite based sign up flow with the passwordless recipe. +sidebar: + order: 70 --- - -# Passwordless login via invite link - ## Overview In this flow, the admin of the app calls an API to sign up a user and send them an invite link. @@ -21,8 +13,8 @@ If a user has not received an invitation before, their sign in attempt fails. ## Before you start -This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +This guide assumes that you have initialized the [Passwordless recipe](/authentication/passwordless/initial-setup), Session, and User Roles, and have a working application integrated with **SuperTokens**. The User Roles recipe protects the invitation endpoint in these examples. +If you have not, please check the [Quickstart Guide](/quickstart). ## Steps @@ -34,13 +26,24 @@ Additionally, protect the endpoint with a role requirement. The `passwordless` API uses the default magic link path, `/auth/verify`, for the invite link. If you are using the pre-built UI, the frontend SDK automatically logs the user in. -For custom UI implementations, use the [`consumeCode` function provided by the frontend SDK](/docs/authentication/passwordless/initial-setup) to call the `passwordless` API that verifies the code in the URL and creates the user. - - - - - - +For custom UI implementations, use the [`consumeCode` function provided by the frontend SDK](/authentication/passwordless/initial-setup#23-consuming-the-magic-link) to verify the code in the URL and authenticate the user created by the invitation endpoint. + +Validate and normalize the email address before creating the user. Configure the framework's JSON body parser before this route; for Koa, expose the parsed payload as `ctx.request.body`. + + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -50,33 +53,34 @@ import Passwordless from "supertokens-node/recipe/passwordless"; let app = express(); -app.post("/create-user", verifySession({ +app.post( + "/create-user", + verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } -}), async (req: SessionRequest, res) => { - let email = req.body.email; + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }), + async (req: SessionRequest, res) => { + let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ - tenantId: "public", - email - }) + tenantId: "public", + email, + }); let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email + tenantId: "public", + email, }); // TODO: send inviteLink to user's email res.send("Success"); -}); + }, +); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -87,42 +91,40 @@ import Passwordless from "supertokens-node/recipe/passwordless"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/create-user", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } - }) - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let email = (req.payload.valueOf() as any).email; - - // this will create the user in supertokens if they don't already exist. - await Passwordless.signInUp({ - tenantId: "public", - email - }) - - let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email - }); - - // TODO: send inviteLink to user's email - res.response("Success").code(200); - } -}) -``` - - + path: "/create-user", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let email = (req.payload.valueOf() as any).email; + + // this will create the user in supertokens if they don't already exist. + await Passwordless.signInUp({ + tenantId: "public", + email, + }); + + let inviteLink = await Passwordless.createMagicLink({ + tenantId: "public", + email, + }); + // TODO: send inviteLink to user's email + res.response("Success").code(200); + }, +}); +``` + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -131,35 +133,36 @@ import Passwordless from "supertokens-node/recipe/passwordless"; let fastify = Fastify(); -fastify.post("/create-user", { +fastify.post( + "/create-user", + { preHandler: verifySession({ - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, }), -}, async (req, res) => { + }, + async (req, res) => { let email = req.body.email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ - tenantId: "public", - email - }) + tenantId: "public", + email, + }); let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email + tenantId: "public", + email, }); // TODO: send inviteLink to user's email res.code(200).send("Success"); -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; @@ -167,37 +170,34 @@ import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; async function createUser(awsEvent: SessionEventV2) { - let email = JSON.parse(awsEvent.body!).email; - - // this will create the user in supertokens if they don't already exist. - await Passwordless.signInUp({ - tenantId: "public", - email - }) - - let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email - }); - - // TODO: send inviteLink to user's email - return { - statusCode: '200', - body: "Success" - } -}; + let email = JSON.parse(awsEvent.body!).email; + + // this will create the user in supertokens if they don't already exist. + await Passwordless.signInUp({ + tenantId: "public", + email, + }); + + let inviteLink = await Passwordless.createMagicLink({ + tenantId: "public", + email, + }); + + // TODO: send inviteLink to user's email + return { + statusCode: "200", + body: "Success", + }; +} exports.handler = verifySession(createUser, { - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -207,34 +207,35 @@ import Passwordless from "supertokens-node/recipe/passwordless"; let router = new KoaRouter(); -router.post("/create-user", verifySession({ +router.post( + "/create-user", + verifySession({ overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } -}), async (ctx: SessionContext, next) => { - let email = (ctx.body as any).email; + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }), + async (ctx: SessionContext, next) => { + let email = ((ctx.request as any).body as { email: string }).email; // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ - tenantId: "public", - email - }) + tenantId: "public", + email, + }); let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email + tenantId: "public", + email, }); // TODO: send inviteLink to user's email ctx.status = 200; ctx.body = "Success"; -}); + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -243,161 +244,108 @@ import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; class LikeComment { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/create-user") - @intercept(verifySession({ - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } - })) - async handler() { - let email = "" // TODO: get from request body - - // this will create the user in supertokens if they don't already exist. - await Passwordless.signInUp({ - tenantId: "public", - email - }) - - let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email - }); - - // TODO: send inviteLink to user's email - // TODO: send 200 response to the client - } -} -``` + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/create-user") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }), + ) + async handler() { + let email = ""; // TODO: get from request body - - + // this will create the user in supertokens if they don't already exist. + await Passwordless.signInUp({ + tenantId: "public", + email, + }); - - - + let inviteLink = await Passwordless.createMagicLink({ + tenantId: "public", + email, + }); + // TODO: send inviteLink to user's email + // TODO: send 200 response to the client + } +} +``` + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; export default async function createUser(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } - })(req, res, next); + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; }, - req, - res - ) + })(req, res, next); + }, + req, + res, + ); - let email = req.body.email; + let email = req.body.email; - // this will create the user in supertokens if they don't already exist. - await Passwordless.signInUp({ - tenantId: "public", - email - }) + // this will create the user in supertokens if they don't already exist. + await Passwordless.signInUp({ + tenantId: "public", + email, + }); - let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email - }); + let inviteLink = await Passwordless.createMagicLink({ + tenantId: "public", + email, + }); - // TODO: send inviteLink to user's email - res.status(200).json({ message: 'Success' }) + // TODO: send inviteLink to user's email + res.status(200).json({ message: "Success" }); } ``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserRoles from "supertokens-node/recipe/userroles"; -import Passwordless from "supertokens-node/recipe/passwordless"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const body = await request.json(); - let email = body.email; - - // this will create the user in supertokens if they don't already exist. - await Passwordless.signInUp({ - tenantId: "public", - email - }) - - let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email - }); - - // TODO: send inviteLink to user's email - return NextResponse.json({ message: 'Success' }); - }, - { - overrideGlobalClaimValidators: async function (globalClaimValidators) { - return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")] - } - }); -} -``` - - - - - -```tsx + + +```tsx check=false reason="This example depends on local application modules." import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import UserRoles from "supertokens-node/recipe/userroles"; import Passwordless from "supertokens-node/recipe/passwordless"; @Controller() export class CreateUserController { - @Post('create-user') - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async function (globalClaimValidators: any) { - return [...globalClaimValidators, - UserRoles.UserRoleClaim.validators.includes("admin")] - } - })) // For more information about this guard please read our NestJS guide. + @Post("create-user") + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async function (globalClaimValidators: any) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }), + ) // For more information about this guard please read our NestJS guide. async postAPI(@Session() session: SessionContainer): Promise { - let email = "" // TODO: get from request body + let email = ""; // TODO: get from request body // this will create the user in supertokens if they don't already exist. await Passwordless.signInUp({ - tenantId: "public", - email - }) + tenantId: "public", + email, + }); let inviteLink = await Passwordless.createMagicLink({ - tenantId: "public", - email + tenantId: "public", + email, }); // TODO: send inviteLink to user's email @@ -405,13 +353,12 @@ export class CreateUserController { } } ``` - - - - - - - + + + + + + ```go import ( "fmt" @@ -430,7 +377,7 @@ func main() { session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { - globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) + globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI).ServeHTTP(rw, r) @@ -442,7 +389,11 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // This will create the user in supertokens if they don't already exist. tenantId := "public" - passwordless.SignInUpByEmail(tenantId, email) + _, err := passwordless.SignInUpByEmail(tenantId, email) + if err != nil { + http.Error(w, "Could not create invited user", http.StatusInternalServerError) + return + } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { @@ -454,10 +405,8 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // TODO: send 200 to the client } ``` - - - - + + ```go import ( "fmt" @@ -478,7 +427,7 @@ func main() { // Wrap the API handler in session.VerifySession router.POST("/create-user", verifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { - globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) + globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }), createUserAPI) @@ -502,7 +451,11 @@ func createUserAPI(c *gin.Context) { // This will create the user in supertokens if they don't already exist. tenantId := "public" - passwordless.SignInUpByEmail(tenantId, email) + _, err := passwordless.SignInUpByEmail(tenantId, email) + if err != nil { + c.String(http.StatusInternalServerError, "Could not create invited user") + return + } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { @@ -514,10 +467,8 @@ func createUserAPI(c *gin.Context) { // TODO: send 200 to the client } ``` - - - - + + ```go import ( "fmt" @@ -538,7 +489,7 @@ func main() { // Wrap the API handler in session.VerifySession r.Post("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { - globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) + globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI)) @@ -549,7 +500,11 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // This will create the user in supertokens if they don't already exist. tenantId := "public" - passwordless.SignInUpByEmail(tenantId, email) + _, err := passwordless.SignInUpByEmail(tenantId, email) + if err != nil { + http.Error(w, "Could not create invited user", http.StatusInternalServerError) + return + } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { @@ -561,10 +516,8 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // TODO: send 200 to the client } ``` - - - - + + ```go import ( "fmt" @@ -585,7 +538,7 @@ func main() { // Wrap the API handler in session.VerifySession router.HandleFunc("/create-user", session.VerifySession(&sessmodels.VerifySessionOptions{ OverrideGlobalClaimValidators: func(globalClaimValidators []claims.SessionClaimValidator, sessionContainer sessmodels.SessionContainer, userContext supertokens.UserContext) ([]claims.SessionClaimValidator, error) { - globalClaimValidators = append(globalClaimValidators, userrolesclaims.PermissionClaimValidators.Includes("admin", nil, nil)) + globalClaimValidators = append(globalClaimValidators, userrolesclaims.UserRoleClaimValidators.Includes("admin", nil, nil)) return globalClaimValidators, nil }, }, createUserAPI)).Methods(http.MethodPost) @@ -596,7 +549,11 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // This will create the user in supertokens if they don't already exist. tenantId := "public" - passwordless.SignInUpByEmail(tenantId, email) + _, err := passwordless.SignInUpByEmail(tenantId, email) + if err != nil { + http.Error(w, "Could not create invited user", http.StatusInternalServerError) + return + } inviteLink, err := passwordless.CreateMagicLinkByEmail(tenantId, email) if err != nil { @@ -608,16 +565,13 @@ func createUserAPI(w http.ResponseWriter, r *http.Request) { // TODO: send 200 to the client } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from fastapi import Depends from supertokens_python.recipe.passwordless.asyncio import create_magic_link, signinup @@ -626,7 +580,7 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.post('/create-user') # type: ignore +@app.post('/create-user') async def create_user(session: SessionContainer = Depends(verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] @@ -642,17 +596,15 @@ async def create_user(session: SessionContainer = Depends(verify_session( # TODO: send invite_link to email # TODO: send 200 responspe to client ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python.recipe.passwordless.syncio import create_magic_link, signinup from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.userroles import UserRoleClaim -@app.route('/create_user', methods=['POST']) # type: ignore +@app.route('/create_user', methods=['POST']) @verify_session( override_global_claim_validators=lambda global_validators, session, user_context: global_validators + [UserRoleClaim.validators.includes("admin")] @@ -669,10 +621,8 @@ def create_user(): # TODO: send invite_link to email # TODO: send 200 responspe to client ``` - - - - + + ```python from django.http import HttpRequest @@ -697,76 +647,135 @@ async def create_user(request: HttpRequest): # TODO: send invite_link to email # TODO: send 200 responspe to client ``` + + + + + + + + + + - - +```tsx check=false reason="This example depends on local application modules." +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserRoles from "supertokens-node/recipe/userroles"; +import Passwordless from "supertokens-node/recipe/passwordless"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const body = await request.json(); + let email = body.email; + + // this will create the user in supertokens if they don't already exist. + await Passwordless.signInUp({ + tenantId: "public", + email, + }); -:::info Multi Tenancy -In the above code snippets, the `"public"` `tenantId` passes when calling the functions - this is the default `tenantId`. If you are using the multi-tenancy feature, you can pass in a different `tenantId` and this ensures that the user with that email adds only to that tenant. + let inviteLink = await Passwordless.createMagicLink({ + tenantId: "public", + email, + }); + + // TODO: send inviteLink to user's email + return NextResponse.json({ message: "Success" }); + }, + { + overrideGlobalClaimValidators: async function (globalClaimValidators) { + return [...globalClaimValidators, UserRoles.UserRoleClaim.validators.includes("admin")]; + }, + }, + ); +} +``` + + + + + + + +:::info[Multi Tenancy] +The examples use the default `public` tenant. In a multi-tenant application, derive an authorized tenant ID from the authenticated administrator's server-side context; do not trust an arbitrary request-body tenant ID. Pass it to both user creation and magic-link creation so the user and invitation belong to the same tenant. You also need to pass in the `tenantId` to the createMagicLink function which adds the `tenantId` to the generated magic link. The resulting link uses the `websiteDomain` configured in the `appInfo` object in `SuperTokens.init`, but you can change the link's domain to match that of the tenant before sending it. ::: -### 2. Check if a user was invited +### 2. Check if a user was invited Update the backend SDK API function to only allow sign up requests from invited users. To do this you need to check if a user exists in **SuperTokens**. - - - + + ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; Passwordless.init({ - contactMethod: "EMAIL", flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // typecheck-only, removed from output - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - createCodePOST: async function (input) { - if ("email" in input) { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - email: input.email - }); - let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined); - if (existingPasswordlessUser === undefined) { - // this is sign up attempt - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } - } - } else { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - phoneNumber: input.phoneNumber - }); - let existingPasswordlessUser = existingUsers.find(user => user.loginMethods.find(lM => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless") !== undefined); - if (existingPasswordlessUser === undefined) { - // this is sign up attempt - return { - status: "GENERAL_ERROR", - message: "Sign up disabled. Please contact the admin." - } - } - } - return await originalImplementation.createCodePOST!(input); - } + contactMethod: "EMAIL_OR_PHONE", + flowType: "MAGIC_LINK", + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + createCodePOST: async function (input) { + if ("email" in input) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email, + }); + let existingPasswordlessUser = existingUsers.find( + (user) => + user.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== + undefined, + ); + if (existingPasswordlessUser === undefined) { + // this is sign up attempt + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin.", + }; } - } - } -}) + } else { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + phoneNumber: input.phoneNumber, + }); + let existingPasswordlessUser = existingUsers.find( + (user) => + user.loginMethods.find( + (lM) => lM.hasSamePhoneNumberAs(input.phoneNumber) && lM.recipeId === "passwordless", + ) !== undefined, + ); + if (existingPasswordlessUser === undefined) { + // this is sign up attempt + return { + status: "GENERAL_ERROR", + message: "Sign up disabled. Please contact the admin.", + }; + } + } + return await originalImplementation.createCodePOST!(input); + }, + }; + }, + }, +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/passwordless" @@ -818,11 +827,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init @@ -902,10 +909,9 @@ def override_passwordless_apis(original_implementation: APIInterface): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ passwordless.init( - contact_config="", # type: ignore # typecheck-only, removed from output flow_type="USER_INPUT_CODE", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, @@ -914,23 +920,20 @@ init( ], ) ``` - - - - - + + --- ## See also - - - - - - - - - + + + + + + + + + diff --git a/docs/authentication/passwordless/meta.ts b/docs/authentication/passwordless/meta.ts new file mode 100644 index 0000000000..4709d1e434 --- /dev/null +++ b/docs/authentication/passwordless/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Passwordless", + icon: "wand-sparkles", + order: 30, +}); diff --git a/docs/authentication/social/_blocks/backend-providers-init.mdx b/docs/authentication/social/_blocks/backend-providers-init.mdx deleted file mode 100644 index 9e3bfadd2f..0000000000 --- a/docs/authentication/social/_blocks/backend-providers-init.mdx +++ /dev/null @@ -1,310 +0,0 @@ - -Populate the `providers` array with the third party authentication providers that you want. - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - ThirdParty.init({ - //highlight-start - signInAndUpFeature: { - // We have provided you with development keys which you can use for testing. - // IMPORTANT: Please replace them with your own OAuth keys for production use. - providers: [{ - config: { - thirdPartyId: "google", - clients: [{ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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" - }] - } - }, { - config: { - thirdPartyId: "github", - clients: [{ - clientId: "467101b197249757c71f", - clientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd" - }] - } - }, { - config: { - thirdPartyId: "apple", - clients: [{ - clientId: "4398792-io.supertokens.example.service", - additionalConfig: { - keyId: "7M48Y4RYDL", - privateKey: - "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - teamId: "YWQCXGJRJL", - } - }] - } - }], - } - //highlight-end - }), - // ... - ] -}); -``` - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - // Inside supertokens.Init - thirdparty.Init(&tpmodels.TypeInput{ - // highlight-start - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - // We have provided you with development keys which you can use for testing. - // IMPORTANT: Please replace them with your own OAuth keys for production use. - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - ClientSecret: "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", - }, - }, - }, - }, - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "github", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "467101b197249757c71f", - ClientSecret: "e97051221f4b6426e8fe8d51486396703012f5bd", - }, - }, - }, - }, - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "apple", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "4398792-io.supertokens.example.service", - AdditionalConfig: map[string]interface{}{ - "keyId": "7M48Y4RYDL", - "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - "teamId": "YWQCXGJRJL", - }, - }, - }, - }, - }, - }, - }, - // highlight-end - }) -} -``` - - - -```python -from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig -from supertokens_python.recipe import thirdparty - -# Inside init -thirdparty.init( - # highlight-start - sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ - # We have provided you with development keys which you can use for testing. - # IMPORTANT: Please replace them with your own OAuth keys for production use. - ProviderInput( - config=ProviderConfig( - third_party_id="google", - clients=[ - ProviderClientConfig( - client_id="1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - client_secret="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", - ), - ], - ), - ), - ProviderInput( - config=ProviderConfig( - third_party_id="github", - clients=[ - ProviderClientConfig( - client_id="467101b197249757c71f", - client_secret="e97051221f4b6426e8fe8d51486396703012f5bd", - ) - ], - ), - ), - ProviderInput( - config=ProviderConfig( - third_party_id="apple", - clients=[ - ProviderClientConfig( - client_id="io.supertokens.example.service", - additional_config={ - "keyId": "7M48Y4RYDL", - "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - "teamId": "YWQCXGJRJL" - }, - ), - ], - ), - ), - ]) - # highlight-end -) -``` - - - - - -:::important -The examples use a set of development keys meant for demonstration purposes only. -Please use your own OAuth credentials. -Read the list of [built-in providers](/docs/authentication/social/built-in-providers-config) that also includes information on how to generate your own keys. -To add a provider that is not listed, you can follow the guide on [setting up custom providers](/docs/authentication/social/custom-providers). -::: - - -#### Set OAuth scopes - -To add additional OAuth scopes when accessing your third-party provider, add them to the configuration when initializing the backend `SDK`. - -For example if you are using Google as your third party provider, you can add an additional scope as follows: - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "google", - clients: [{ - clientId: "TODO: GOOGLE_CLIENT_ID", - clientSecret: "TODO: GOOGLE_CLIENT_SECRET", - scope: ["scope1", "scope2"] - }] - } - } - ] - } - }) - ] -}); -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO: GOOGLE_CLIENT_ID", - ClientSecret: "TODO: GOOGLE_CLIENT_SECRET", - Scope: []string{ - "scope1", "scope2", - }, - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="google", - clients=[ - ProviderClientConfig( - client_id="GOOGLE_CLIENT_ID", - client_secret="GOOGLE_CLIENT_SECRET", - scope=["scope1", "scope2"] - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - -:::important -Along with your custom scopes, also add scopes that ask for the user's email and its verification status. For example, with Google, this scope is `"https://www.googleapis.com/auth/userinfo.email"`. -::: diff --git a/docs/authentication/social/_blocks/backend-sdk-init.mdx b/docs/authentication/social/_blocks/backend-sdk-init.mdx deleted file mode 100644 index 1eb794bd37..0000000000 --- a/docs/authentication/social/_blocks/backend-sdk-init.mdx +++ /dev/null @@ -1,132 +0,0 @@ - -You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. -The init call includes [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. - - - - - - - -```tsx title="Backend SDK Init" showAppTypeSelect -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -// highlight-next-line -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -supertokens.init({ - // Replace this with the framework you are using - framework: "express", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - // highlight-start - ThirdParty.init({/*TODO: See next step*/}), - // highlight-end - Session.init() - ] -}); -``` - - - - - - - - - - - -```python title="Backend SDK Init" showAppTypeSelect -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import thirdparty, session -# highlight-next-line - -init( - app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" - ), - supertokens_config=SupertokensConfig( - # We use try.supertokens for demo purposes. - # At the end of the tutorial we will show you how to create - # your own SuperTokens core instance and then update your config. - connection_uri="https://try.supertokens.io^{derived.appIdPathname}", - # api_key: - ), - framework='fastapi', - recipe_list=[ - session.init(), # initializes session features - thirdparty.init( - # TODO: See next step - ) # type: ignore - ], - mode='asgi' # use wsgi if you are running using gunicorn -) -``` - - - - - - - - -```go title="Backend SDK Init" showAppTypeSelect -import ( - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - apiBasePath := "^{appInfo.apiBasePath}" - websiteBasePath := "^{appInfo.websiteBasePath}" - err := supertokens.Init(supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - ConnectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // APIKey: - }, - AppInfo: supertokens.AppInfo{ - AppName: "^{appInfo.appName}", - APIDomain: "^{appInfo.apiDomain}", - WebsiteDomain: "^{appInfo.websiteDomain}", - APIBasePath: &apiBasePath, - WebsiteBasePath: &websiteBasePath, - }, - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{/*TODO: See next step*/}), - session.Init(nil), // initializes session features - }, - }) - - if err != nil { - panic(err.Error()) - } -} -``` - - - - diff --git a/docs/authentication/social/_blocks/frontend-custom-ui-mobile-access-token-flow.mdx b/docs/authentication/social/_blocks/frontend-custom-ui-mobile-access-token-flow.mdx deleted file mode 100644 index 246b69644d..0000000000 --- a/docs/authentication/social/_blocks/frontend-custom-ui-mobile-access-token-flow.mdx +++ /dev/null @@ -1,44 +0,0 @@ - -### Fetching the OAuth/Access tokens on the frontend - -1. Sign in with the social provider. The minimum required scope is the one that provides access to the user's email. You can use any library to sign in with the social provider. -2. Get the access token on the frontend if it is available. -3. Get the id token from the sign in result if it is available. - -:::important -You need to provide either the access token or the id token, or both in step 2, depending on what is available. -::: - -### Calling the `signinup` API to use the OAuth tokens - - -Once you have the `access_token` or the `id_token` from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "thirdPartyId": "google", - "clientType": "...", - "oAuthTokens": { - "access_token": "...", - "id_token": "..." - }, -}' -``` - -:::important -- The `clientType` input is optional, and you need it only if you have initialised more than one client in the provide on the backend (See the "Social / Single Sign-On login for both, web and mobile apps" section below). -- If you have the `id_token`, you can send that along with the `access_token`. -::: - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This occurs if the social / Single Sign-On provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. -- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -:::note -On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. -::: - diff --git a/docs/authentication/social/_blocks/frontend-custom-ui-mobile-authorization-code-flow.mdx b/docs/authentication/social/_blocks/frontend-custom-ui-mobile-authorization-code-flow.mdx deleted file mode 100644 index 6b134898a9..0000000000 --- a/docs/authentication/social/_blocks/frontend-custom-ui-mobile-authorization-code-flow.mdx +++ /dev/null @@ -1,666 +0,0 @@ - -### Sign in with Apple example - - - - - - - -#### Fetching the authorisation token on the frontend - -For react native apps, this involves setting up the [react-native-apple-authentication library](https://github.com/invertase/react-native-apple-authentication) in your app. Check out their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. In the case of Apple, that could be the user's actual email or the proxy email provided by Apple - it doesn't matter. - -Once the integration is complete, you should call the `appleAuth.performRequest` function for iOS and the `appleAuthAndroid.signIn` function for Android. Either way, the result of the function is a one-time use auth code which you should send to your backend as shown in the next step. - -A full example of this is available in [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/apple.ts). - -In case you are using Expo, you can use the [expo-apple-authentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) library instead (not that this library only works on iOS). - - - - - - - - - - -#### Fetching the authorisation token on the frontend - -:::info -At the moment this flow is not supported on Android. -::: - - - - - - - - - -#### Fetching the authorisation token on the frontend - -For iOS you use the normal sign in with apple flow and then use the authorization code to login with SuperTokens. You can see a full example of this in the `onAppleClicked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). - -```swift -import UIKit -import AuthenticationServices - -fileprivate class ViewController: UIViewController, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate { - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return view.window! - } - - func loginWithApple() { - let authorizationRequest = ASAuthorizationAppleIDProvider().createRequest() - authorizationRequest.requestedScopes = [.email, .fullName] - - let authorizationController = ASAuthorizationController(authorizationRequests: [authorizationRequest]) - - authorizationController.presentationContextProvider = self - authorizationController.delegate = self - authorizationController.performRequests() - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - guard let credential: ASAuthorizationAppleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, - let authorizationCode = credential.authorizationCode, - let authorizationCodeString: String = String(data: authorizationCode, encoding: .utf8), - let email: String = credential.email as? String, - let nameComponents: PersonNameComponents = credential.fullName as? PersonNameComponents, - let firstName: String = nameComponents.givenName as? String, - let lastName: String = nameComponents.familyName as? String else {return} - - // Send the user information and auth code to the backend. Refer to the next step. - } -} -``` - - - - - - - - - -#### Fetching the authorisation token on the frontend - -For flutter, use the [`sign_in_with_apple`](https://pub.dev/packages/sign_in_with_apple) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the apple sign-in flow. You can see a full example of this in the `loginWithApple` function in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). - -```dart -import 'package:sign_in_with_apple/sign_in_with_apple.dart'; - -void loginWithApple() async { - try { - var credential = await SignInWithApple.getAppleIDCredential( - scopes: [ - AppleIDAuthorizationScopes.email, - AppleIDAuthorizationScopes.fullName, - ], - // Required for Android only - webAuthenticationOptions: WebAuthenticationOptions( - clientId: "", - redirectUri: Uri.parse( - "//callback/apple", - ), - ), - ); - - String authorizationCode = credential.authorizationCode; - String? idToken = credential.identityToken; - String? email = credential.email; - String? firstname = credential.givenName; - String? lastName = credential.familyName; - - // Send the user information and auth code to the backend. Refer to the next step. - } catch (e) { - // Sign in aborted or failed - } -} -``` - -In the snippet above for Android, you need an additional `webAuthenticationOptions` property when signing in with Apple. -This is because on Android the library uses the web login flow and requires the client id and redirection URI. -The `redirectUri` property here is the URL to which Apple makes a `POST` request after the user has logged in. -The SuperTokens backend SDKs provide an API for this at `//callback/apple`. - -#### Additional steps for Android - -For android, a way for the web login flow to redirect back to the app is also needed. By default, the API provided by the backend `SDKs` redirects to the website domain you provide when initializing the `SDK`. The API can be overridden to redirect to the app instead. For example, if using the Node.js `SDK`: - - - - - -```tsx -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -ThirdParty.init({ - // highlight-start - override: { - apis: (original) => { - return { - ...original, - appleRedirectHandlerPOST: async (input) => { - if (original.appleRedirectHandlerPOST === undefined) { - throw Error("Should never come here"); - } - - // inut.formPostInfoFromProvider contains all the query params attached by Apple - const stateInBase64 = input.formPostInfoFromProvider.state; - - // The web SDKs add a default state - if (stateInBase64 === undefined) { - // Redirect to android app - // We create a dummy URL to create the query string - const dummyUrl = new URL("http://localhost:8080"); - for (const [key, value] of Object.entries(input.formPostInfoFromProvider)) { - dummyUrl.searchParams.set(key, `${value}`); - } - - const queryString = dummyUrl.searchParams.toString(); - // Refer to the README of sign_in_with_apple to understand what this url is - const redirectUrl = `intent://callback?${queryString}#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end`; - - input.options.res.setHeader("Location", redirectUrl, false); - input.options.res.setStatusCode(303); - input.options.res.sendHTMLResponse(""); - } else { - // For the web flow we can use the original implementation - original.appleRedirectHandlerPOST(input); - } - }, - }; - }, - }, - // highlight-end -}) -``` - - - - - -```go -import ( - "net/http" - "strings" - - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - thirdparty.Init(&tpmodels.TypeInput{ - Override: &tpmodels.OverrideStruct{ - // highlight-start - APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { - originalAppleRedirectPost := *originalImplementation.AppleRedirectHandlerPOST - - *originalImplementation.AppleRedirectHandlerPOST = func(formPostInfoFromProvider map[string]interface{}, options tpmodels.APIOptions, userContext *map[string]interface{}) error { - // formPostInfoFromProvider contains all the query params attached by Apple - state, stateOk := formPostInfoFromProvider["state"].(string) - - queryParams := []string{} - if (!stateOk) || state == "" { - // Redirect to android app - for key, value := range formPostInfoFromProvider { - queryParams = append(queryParams, key+"="+value.(string)) - } - - queryString := "" - if len(queryParams) > 0 { - queryString = strings.Join(queryParams, "&") - } - - // Refer to the README of sign_in_with_apple to understand what this url is - redirectUri := "intent://callback?" + queryString + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" - - options.Res.Header().Set("Location", redirectUri) - options.Res.WriteHeader(http.StatusSeeOther) - return nil - } else { - return originalAppleRedirectPost(formPostInfoFromProvider, options, userContext) - } - } - - return originalImplementation - }, - }, - // highlight-end - }) -} -``` - - - - - -```python -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty.interfaces import APIInterface, APIOptions -from typing import Dict, Any - -# highlight-start -def override_thirdparty_apis(original_implementation: APIInterface): - original_apple_redirect_post = original_implementation.apple_redirect_handler_post - - async def apple_redirect_handler_post( - form_post_info: Dict[str, Any], - api_options: APIOptions, - user_context: Dict[str, Any] - ): - # form_post_info contains all the query params attached by Apple - state = form_post_info["state"] - - # The web SDKs add a default state - if state is None: - query_items = [ - f"{key}={value}" for key, value in form_post_info.items() - ] - - query_string = "&".join(query_items) - - # Refer to the README of sign_in_with_apple to understand what this url is - redirect_url = "intent://callback?" + query_string + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" - - api_options.response.set_header("Location", redirect_url) - api_options.response.set_status_code(303) - api_options.response.set_html_content("") - else: - return await original_apple_redirect_post(form_post_info, api_options, user_context) - - original_implementation.apple_redirect_handler_post = apple_redirect_handler_post - return original_implementation -# highlight-end - -thirdparty.init( - # highlight-start - override=thirdparty.InputOverrideConfig( - apis=override_thirdparty_apis - ), - # highlight-end -) -``` - - - - - -In the code above, the `appleRedirectHandlerPOST` API is overridden to check if the request came from the Android app. You can skip checking the state if you only have a mobile app and no website. `sign_in_with_apple` requires parsing the query params sent by Apple and including them in the redirect URL in a specific way. Then, redirect to the deep link URL. Refer to the README for `sign_in_with_apple` to read about the deep link setup required in Android. - - - - - - - -#### Calling the `signinup` API to consume the authorisation token - - - -Once you have the authorization code from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "thirdPartyId": "apple", - "clientType": "...", - "redirectURIInfo": { - "redirectURIOnProviderDashboard": "^{appInfo.apiDomain}^{appInfo.apiBasePath}/callback/apple", - "redirectURIQueryParams": { - "code": "...", - "user": { - "name":{ - "firstName":"...", - "lastName":"..." - }, - "email":"..." - } - } - } -}' -``` - -:::important -- On iOS, the client id set in the backend should be the same as the bundle identifier for your app. -- The `clientType` input is optional and required only if you initialize more than one client in the provider on the backend (See the "Social / `SSO` login for both, web and mobile apps" section below). -- On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. -- On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. -- The `user` object contains information provided by Apple. -::: - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. -- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -:::note -On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. -::: - - -### Sign in with Google example - - - - - - - -#### Fetching the authorisation token on the frontend - -This involves setting up the [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) in your app. Checkout their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. - -Once you configure the library, use `GoogleSignin.configure` and `GoogleSignin.signIn` to trigger the login flow and sign the user in with Google. Refer to [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/google.ts) to see the full code for this. - -```tsx -import { GoogleSignin } from "@react-native-google-signin/google-signin"; - -export const performGoogleSignIn = async (): Promise => { - GoogleSignin.configure({ - webClientId: "GOOGLE_WEB_CLIENT_ID", - iosClientId: "GOOGLE_IOS_CLIENT_ID", - }); - - try { - const response = await GoogleSignin.signIn({}); - const authCode = response.data?.serverAuthCode; - - // Refer to step 2 - - return true; - } catch (e) { - console.log("Google sign in failed with error", e); - } - - return false; -}; -``` - - - - - - - - - - -#### Fetching the authorisation token on the frontend - -Follow the [official Google Sign In guide](https://developers.google.com/identity/sign-in/android/start-integrating) to set up their library and sign the user in with Google. Fetch the authorization code from the Google sign-in result. For a full example, refer to the `signInWithGoogle` function in [the example app](https://github.com/supertokens/supertokens-android/blob/master/examples/with-thirdparty/app/src/main/java/com/supertokens/supertokensexample/LoginActivity.kt). - -```kotlin -import androidx.activity.result.ActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import android.util.Log -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import android.content.Intent - -class LoginActivity : AppCompatActivity() { - private lateinit var googleResultLauncher: ActivityResultLauncher - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - googleResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - onGoogleResultReceived(it) - } - } - - private fun signInWithGoogle() { - val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) - .requestServerAuthCode("GOOGLE_WEB_CLIENT_ID") - .requestEmail() - .build() - - val googleClient = GoogleSignIn.getClient(this, gso) - val signInIntent = googleClient.signInIntent - - googleResultLauncher.launch(signInIntent) - } - - private fun onGoogleResultReceived(it: ActivityResult) { - val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) - val account = task.result - val authCode = account.serverAuthCode - - // Refer to step 2 - } -} -``` - - - - - - - - - -#### Fetching the authorisation token on the frontend - -For iOS, use the `GoogleSignIn` library. Follow the [official guide](https://developers.google.com/identity/sign-in/ios/start-integrating) to set up the library and sign the user in with Google. Use the result of Google sign-in to get the authorization code. For a full example, refer to the `onGoogleCliked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). - -```swift -import UIKit -import GoogleSignIn - -fileprivate class LoginScreenViewController: UIViewController { - @IBAction func onGoogleCliked() { - GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in - guard error == nil else { return } - - guard let authCode: String = signInResult?.serverAuthCode as? String else { - print("Google login did not return an authorisation code") - return - } - - // Refer to step 2 - } - } -} -``` - - - - - - - - - -#### Fetching the authorisation token on the frontend - -For flutter, use the [`google_sign_in`](https://pub.dev/packages/google_sign_in) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the Google sign-in flow. For a full example, refer to the `loginWithGoogle` in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). - -```dart -import 'package:google_sign_in/google_sign_in.dart'; -import 'dart:io'; - -Future loginWithGoogle() async { - GoogleSignIn googleSignIn; - - if (Platform.isAndroid) { - googleSignIn = GoogleSignIn( - serverClientId: "GOOGLE_WEB_CLIENT_ID", - scopes: [ - 'email', - ], - ); - } else { - googleSignIn = GoogleSignIn( - clientId: "GOOGLE_IOS_CLIENT_ID", - serverClientId: "GOOGLE_WEB_CLIENT_ID", - scopes: [ - 'email', - ], - ); - } - - GoogleSignInAccount? account = await googleSignIn.signIn(); - - if (account == null) { - print("Google sign in was aborted"); - return; - } - - String? authCode = account.serverAuthCode; - - if (authCode == null) { - print("Google sign in did not return a server auth code"); - return; - } - - // Refer to step 2 - } -``` - - - - - - - -

Step 2) Calling the `signinup` API to consume the authorisation token

- - - -Once you have the authorization code from the auth provider, you need to call the `signinup` API exposed by the backend `SDK` as shown below: - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "thirdPartyId": "google", - "clientType": "...", - "redirectURIInfo": { - "redirectURIOnProviderDashboard": "", - "redirectURIQueryParams": { - "code": "...", - } - } -}' -``` - -:::important -When calling the API exposed by the SuperTokens backend `SDK`, pass an empty string for `redirectURIOnProviderDashboard`. -The native login flow using the authorization code does not involve any redirection on the frontend. -::: - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. -- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -:::note -On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. -::: - - -### Authorization code grant flow with `PKCE` - -This is similar to the first one, except that you do **not** need to provide a client secret during backend init. -This flow only works for providers which support the [`PKCE` flow](https://oauth.net/2/pkce/). - - - - - - - -#### Fetching the authorisation token on the frontend - -You can use the [react native auth library](https://github.com/FormidableLabs/react-native-app-auth) to also return the `PKCE` code verifier along with the authorization code. Achieve this by setting the `usePKCE` boolean to `true` and also by setting the `skipCodeExchange` to `true` when configuring the react native auth library. - - - - - - - - - - -#### Fetching the authorisation token on the frontend - -You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library to use the `PKCE` flow by using the `setCodeVerifier` method when creating a `AuthorizationRequest`. - - - - - - - - - -#### Fetching the authorisation token on the frontend - -You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library to use the `PKCE` flow. - - - - - - - - - - -#### Fetching the authorisation token on the frontend - -You can use [`flutter_appauth`](https://pub.dev/packages/flutter_appauth) to use the `PKCE` flow by providing a `codeVerifier` when you call the `appAuth.token` function. - - - - - - - -#### Calling the `signinup` API to consume the authorisation token - - - -Once you have the authorization code and `PKCE` verifier from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signinup' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "thirdPartyId": "THIRD_PARTY_ID", - "clientType": "...", - "redirectURIInfo": { - "redirectURIOnProviderDashboard": "REDIRECT_URI", - "redirectURIQueryParams": { - "code": "...", - }, - "pkceCodeVerifier": "..." - } -}' -``` - -:::important -- Replace `THIRD_PARTY_ID` with the provider id. The provider id must match the one you configure in the backend when initialising SuperTokens. -- `REDIRECT_URI` must exactly match the value you configure on the providers dashboard. -::: - -The response body from the API call has a `status` property in it: -- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. -- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: This status occurs if the social / `SSO` provider did not provide an email for the user. In this case, you want to ask the user to pick another method of sign in. Or, you can also override the backend functions to create a fake email for the user for this provider. -- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. -- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. - -:::note -On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you. -::: diff --git a/docs/authentication/social/_blocks/frontend-custom-ui-web-authorization-code-flow.mdx b/docs/authentication/social/_blocks/frontend-custom-ui-web-authorization-code-flow.mdx deleted file mode 100644 index 0997e98f23..0000000000 --- a/docs/authentication/social/_blocks/frontend-custom-ui-web-authorization-code-flow.mdx +++ /dev/null @@ -1,261 +0,0 @@ - -#### Redirecting to a social/single sign-on provider - -The first step is to fetch the URL on which the user authenticates. You can do this by querying the backend API exposed by SuperTokens (as shown below). The backend SDK automatically appends the right query params to the URL (like scope, client ID etc). - -After getting the URL, redirect the user there. In the code below, an example of login with Google appears: - - - - - -```tsx -import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; - -async function googleSignInClicked() { - try { - const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: "google", - - // This is where Google should redirect the user back after login or error. - // This URL goes on the Google's dashboard as well. - frontendRedirectURI: "http:///auth/callback/google", - }); - - /* - Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow - */ - - // we redirect the user to google for auth. - window.location.assign(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; -async function googleSignInClicked() { - try { - const authUrl = await supertokensThirdParty.getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: "google", - - // This is where Google should redirect the user back after login or error. - // This URL goes on the Google's dashboard as well. - frontendRedirectURI: "http:///auth/callback/google", - }); - - /* - Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow - */ - - // we redirect the user to google for auth. - window.location.assign(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -#### Handling the auth callback on your frontend - -Once the third party provider redirects your user back to your app, you need to consume the information to sign in the user. This requires you to: -- Setup a route in your app that handles this callback. It's recommended to use something like `http:///auth/callback/google` (for Google). Regardless of what you make this path, remember to use that same path when calling the `getAuthorisationURLWithQueryParamsAndSetState` function in the first step. - -- On that route, call the following function on page load - - - - -```tsx -import { signInAndUp } from "supertokens-web-js/recipe/thirdparty"; - -async function handleGoogleCallback() { - try { - const response = await signInAndUp(); - - if (response.status === "OK") { - console.log(response.user) - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // sign up successful - } else { - // sign in successful - } - window.location.assign("/home"); - } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // SuperTokens requires that the third party provider - // gives an email for the user. If that's not the case, sign up / in - // will fail. - - // As a hack to solve this, you can override the backend functions to create a fake email for the user. - - window.alert("No email provided by social login. Please use another form of login"); - window.location.assign("/auth"); // redirect back to login page - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; -async function handleGoogleCallback() { - try { - const response = await supertokensThirdParty.signInAndUp(); - - if (response.status === "OK") { - console.log(response.user) - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // sign up successful - } else { - // sign in successful - } - window.location.assign("/home"); - } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in / up was not allowed. - window.alert(response.reason) - } else { - // SuperTokens requires that the third party provider - // gives an email for the user. If that's not the case, sign up / in - // will fail. - - // As a hack to solve this, you can override the backend functions to create a fake email for the user. - - window.alert("No email provided by social login. Please use another form of login"); - window.location.assign("/auth"); // redirect back to login page - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -:::note -On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend SDK. -::: - -#### Special case for login with Apple - - - -Unlike other providers, Apple does not redirect your user back to your frontend app. Instead, it redirects the user to your backend with a `FORM POST` request. This means that the URL you configure on Apple's dashboard should point to your backend API layer. Here, **middleware** handles the request and redirects the user to your frontend app. Your frontend app should then call the `signInAndUp` API on that page as shown previously. - -To tell SuperTokens which frontend route to redirect the user back to, set the `frontendRedirectURI` to the frontend route. Also, set the `redirectURIOnProviderDashboard` to point to your backend API route, to which Apple sends a `POST` request. - - - - -```tsx -import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; - -async function appleSignInClicked() { - try { - const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: "apple", - - frontendRedirectURI: "http://localhost:3000/auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. - // highlight-start - redirectURIOnProviderDashboard: "^{appInfo.apiDomain}^{appInfo.apiBasePath}/callback/apple", // This URL goes on the Apple's dashboard - // highlight-end - }); - - // we redirect the user to apple for auth. - window.location.assign(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - -```tsx -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; -async function appleSignInClicked() { - try { - const authUrl = await supertokensThirdParty.getAuthorisationURLWithQueryParamsAndSetState({ - thirdPartyId: "apple", - - frontendRedirectURI: "http://localhost:3000/auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. - // highlight-start - redirectURIOnProviderDashboard: "^{appInfo.apiDomain}^{appInfo.apiBasePath}/callback/apple", // This URL goes on the Apple's dashboard - // highlight-end - }); - - /* - Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow - */ - - // we redirect the user to google for auth. - window.location.assign(authUrl); - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - -:::info - -If you are using the **Authorization Code Grant** flow with **PKCE** you do **not** need to provide a client secret during backend init. -This only works for providers which support the [PKCE flow](https://oauth.net/2/pkce/). - -::: - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-active-directory.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-active-directory.mdx deleted file mode 100644 index 2d0e7f6c9f..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-active-directory.mdx +++ /dev/null @@ -1,99 +0,0 @@ - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "active-directory", - name: "Active Directory", - clients: [{ - clientId: "...", - clientSecret: "...", - }], - oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "active-directory", - Name: "Active Directory", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="active-directory", - name="Active Directoy", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-apple.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-apple.mdx deleted file mode 100644 index 616ed35210..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-apple.mdx +++ /dev/null @@ -1,185 +0,0 @@ - -Call the following function / API to add the third party provider to a specific tenant. - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "apple", - name: "Apple", - clients: [{ - clientId: "...", - additionalConfig: { - "keyId": "...", - "privateKey": "...", - "teamId": "...", - } - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "apple", - Name: "Apple", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - AdditionalConfig: map[string]interface{}{ - "keyId": "...", - "privateKey": "...", - "teamId": "...", - }, - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="apple", - name="Apple", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "keyId": "...", - "privateKey": "...", - "teamId": "...", - }, - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="apple", - name="Apple", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "keyId": "...", - "privateKey": "...", - "teamId": "...", - }, - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "apple", - "name": "Apple", - "clients": [ - { - "clientId": "...", - "additionalConfig": { - "keyId": "...", - "privateKey": "...", - "teamId": "..." - } - } - ] - } -}' -``` - - - - - -Create Apple Provider - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-bitbucket.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-bitbucket.mdx deleted file mode 100644 index a5b46ae719..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-bitbucket.mdx +++ /dev/null @@ -1,111 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "google", - clients: [{ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - ClientSecret: "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", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="google", - clients=[ - ProviderClientConfig( - client_id="1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - client_secret="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", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-discord.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-discord.mdx deleted file mode 100644 index 0cc994795d..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-discord.mdx +++ /dev/null @@ -1,161 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "discord", - name: "Discord", - clients: [{ - clientId: "...", - clientSecret: "...", - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "discord", - Name: "Discord", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="discord", - name="Discord", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="discord", - name="Discord", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "discord", - "name": "Discord", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ] - } -}' -``` - - - - - -Create Discord Provider - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-facebook.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-facebook.mdx deleted file mode 100644 index aef8466715..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-facebook.mdx +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "facebook", - name: "Facebook", - clients: [{ - clientId: "...", - clientSecret: "...", - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "facebook", - Name: "Facebook", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="facebook", - name="Facebook", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="facebook", - name="Facebook", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "facebook", - "name": "Facebook", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ] - } -}' -``` - - - - - -Create Facebook Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-github.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-github.mdx deleted file mode 100644 index f37acf4314..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-github.mdx +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "github", - name: "GitHub", - clients: [{ - clientId: "...", - clientSecret: "...", - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "github", - Name: "GitHub", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="github", - name="GitHub", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="github", - name="GitHub", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "github", - "name": "GitHub", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ] - } -}' -``` - - - - - - -Create GitHub Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-gitlab.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-gitlab.mdx deleted file mode 100644 index 91ad2d0137..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-gitlab.mdx +++ /dev/null @@ -1,168 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "gitlab", - name: "GitLab", - clients: [{ - clientId: "...", - clientSecret: "...", - }], - oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "gitlab", - Name: "GitLab", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="gitlab", - name="Gitlab", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="gitlab", - name="Gitlab", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "gitlab", - "name": "GitLab", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ], - "oidcDiscoveryEndpoint": "https://gitlab.example.com/.well-known/openid-configuration" - } -}' -``` - - - - - - -Create GitLab Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-google-workspaces.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-google-workspaces.mdx deleted file mode 100644 index 71b7b95ac2..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-google-workspaces.mdx +++ /dev/null @@ -1,174 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "google-workspaces", - name: "Google Workspaces", - clients: [{ - clientId: "...", - clientSecret: "...", - additionalConfig: { - "hd": "example.com", - } - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "google-workspaces", - Name: "Google Workspaces", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - AdditionalConfig: map[string]interface{}{ - "hd": "example.com", - }, - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="google-workspaces", - name="Google Workspaces", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "hd": "example.com", - }, - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="google-workspaces", - name="Google Workspaces", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "hd": "example.com", - }, - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "google-workspaces", - "name": "Google Workspaces", - "clients": [ - { - "clientId": "...", - "clientSecret": "...", - "additionalConfig": { - "hd": "example.com" - } - } - ] - } -}' -``` - - - - - -Create Google Provider - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-google.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-google.mdx deleted file mode 100644 index de0aacb193..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-google.mdx +++ /dev/null @@ -1,155 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "google", - name: "Google", - clients: [{ - clientId: "...", - clientSecret: "..." - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Name: "Google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="google", - name="Google", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="google", - name="Google", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "google", - "name": "Google", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ] - } -}' -``` - - - - - -Create Google Provider - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-linkedin.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-linkedin.mdx deleted file mode 100644 index eef90550d3..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-linkedin.mdx +++ /dev/null @@ -1,162 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "linkedin", - name: "LinkedIn", - clients: [{ - clientId: "...", - clientSecret: "...", - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "linkedin", - Name: "LinkedIn", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="linkedin", - name="LinkedIn", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="linkedin", - name="LinkedIn", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "linkedin", - "name": "LinkedIn", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ] - } -}' -``` - - - - - -Create LinkedIn Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-okta.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-okta.mdx deleted file mode 100644 index ea3c3d2ac8..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-okta.mdx +++ /dev/null @@ -1,169 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "okta", - name: "Okta", - clients: [{ - clientId: "...", - clientSecret: "...", - }], - oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "okta", - Name: "Okta", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - }, - }, - OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="okta", - name="Okta", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="okta", - name="Okta", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - ), - ], - oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "okta", - "name": "Okta", - "clients": [ - { - "clientId": "...", - "clientSecret": "..." - } - ], - "oidcDiscoveryEndpoint": "https://dev-.okta.com/.well-known/openid-configuration" - } -}' -``` - - - - - - -Create Okta Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-saml.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-saml.mdx deleted file mode 100644 index 7bd810501a..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-saml.mdx +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "boxy-saml", - name: "", - clients: [{ - clientId: "...", - clientSecret: "...", - additionalConfig: { - "boxyURL": "", - } - }], - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "boxy-saml", - Name: "", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "...", - ClientSecret: "...", - AdditionalConfig: map[string]interface{}{ - "boxyURL": "", - }, - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="boxy-saml", - name="", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "boxyURL": "", - }, - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="boxy-saml", - name="", - clients=[ - ProviderClientConfig( - client_id="...", - client_secret="...", - additional_config={ - "boxyURL": "", - }, - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "boxy-SAML", - "name": "", - "clients": [ - { - "clientId": "...", - "clientSecret": "...", - "additionalConfig": { - "boxyURL": "" - } - } - ] - } -}' -``` - - - - - - -Create SAML Provider - - - - - diff --git a/docs/authentication/social/_blocks/multi-tenant-provider-config-twitter.mdx b/docs/authentication/social/_blocks/multi-tenant-provider-config-twitter.mdx deleted file mode 100644 index b5810d51c2..0000000000 --- a/docs/authentication/social/_blocks/multi-tenant-provider-config-twitter.mdx +++ /dev/null @@ -1,164 +0,0 @@ - - - - - -```tsx -import Multitenancy from "supertokens-node/recipe/multitenancy"; - -async function addThirdPartyToTenant() { - let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { - thirdPartyId: "twitter", - name: "Twitter", - clients: [{ - clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - }] - }); - - if (resp.createdNew) { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/multitenancy" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" -) - -func main() { - tenantId := "customer1" - - // highlight-start - resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ - ThirdPartyId: "twitter", - Name: "Twitter", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - }, - }, - }, nil) - // highlight-end - - if err != nil { - // handle error - } - if resp.OK.CreatedNew { - // Provider added to customer1 - } else { - // Existing provider config overwritten for customer1 - } -} -``` - - - - - - - - - -```python -from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -async def some_func(): - tenant_id = "customer1" - result = await create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="twitter", - name="Twitter", - clients=[ - ProviderClientConfig( - client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - ), - ], - )) - - if result.status != "OK": - print("handle error") - elif result.created_new: - print("Provider added to customer1") - else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - -```python -from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config -from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig - -tenant_id = "customer1" -result = create_or_update_third_party_config(tenant_id, ProviderConfig( - third_party_id="twitter", - name="Twitter", - clients=[ - ProviderClientConfig( - client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - ), - ], -)) - -if result.status != "OK": - print("handle error") -elif result.created_new: - print("Provider added to customer1") -else: - print("Existing provider config overwritten for customer1") -``` - - - - - - - - - - -```bash -curl --location --request PUT '^{coreInfo.uri}//recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "config": { - "thirdPartyId": "twitter", - "name": "Twitter", - "clients": [ - { - "clientId": "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - "clientSecret": "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC" - } - ] - } -}' -``` - - - - - - -Create Twitter Provider - - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-active-directory.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-active-directory.mdx deleted file mode 100644 index 3f981a5fa8..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-active-directory.mdx +++ /dev/null @@ -1,118 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "active-directory", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }], - oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "active-directory", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="active-directory", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-apple.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-apple.mdx deleted file mode 100644 index af8bb5441f..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-apple.mdx +++ /dev/null @@ -1,127 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "apple", - clients: [{ - clientId: "4398792-io.supertokens.example.service", - additionalConfig: { - "keyId": "7M48Y4RYDL", - "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - "teamId": "YWQCXGJRJL", - }, - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "apple", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "4398792-io.supertokens.example.service", - AdditionalConfig: map[string]interface{}{ - "keyId": "7M48Y4RYDL", - "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - "teamId": "YWQCXGJRJL", - }, - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="apple", - clients=[ - ProviderClientConfig( - client_id="4398792-io.supertokens.example.service", - additional_config={ - "keyId": "7M48Y4RYDL", - "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", - "teamId": "YWQCXGJRJL", - }, - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-bitbucket.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-bitbucket.mdx deleted file mode 100644 index a5b46ae719..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-bitbucket.mdx +++ /dev/null @@ -1,111 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "google", - clients: [{ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - ClientSecret: "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", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="google", - clients=[ - ProviderClientConfig( - client_id="1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - client_secret="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", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-discord.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-discord.mdx deleted file mode 100644 index 4c1ed47157..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-discord.mdx +++ /dev/null @@ -1,115 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "discord", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "discord", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="discord", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-facebook.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-facebook.mdx deleted file mode 100644 index 1386dabbe6..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-facebook.mdx +++ /dev/null @@ -1,115 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "facebook", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "facebook", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="facebook", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-github.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-github.mdx deleted file mode 100644 index 79a77f0734..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-github.mdx +++ /dev/null @@ -1,115 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "github", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "github", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="github", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-gitlab.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-gitlab.mdx deleted file mode 100644 index 3ed993fd4c..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-gitlab.mdx +++ /dev/null @@ -1,118 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "gitlab", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }], - oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "gitlab", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="gitlab", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-google-workspaces.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-google-workspaces.mdx deleted file mode 100644 index 11c5e52333..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-google-workspaces.mdx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "google-workspaces", - clients: [{ - clientId: "TODO", - clientSecret: "TODO", - additionalConfig: { - "hd": "example.com" - } - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google-workspaces", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - AdditionalConfig: map[string]interface{}{ - "hd": "example.com", - }, - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="google-workspaces", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - additional_config={ - "hd": "example.com", - }, - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-google.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-google.mdx deleted file mode 100644 index a5b46ae719..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-google.mdx +++ /dev/null @@ -1,111 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "google", - clients: [{ - clientId: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - clientSecret: "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", - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "google", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - ClientSecret: "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", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="google", - clients=[ - ProviderClientConfig( - client_id="1060725074195-kmeum4crr01uirfl2op9kd5acmi9jutn.apps.googleusercontent.com", - client_secret="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", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-linkedin.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-linkedin.mdx deleted file mode 100644 index af9d80c33a..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-linkedin.mdx +++ /dev/null @@ -1,115 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "linkedin", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "linkedin", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="linkedin", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-okta.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-okta.mdx deleted file mode 100644 index a72e9f329d..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-okta.mdx +++ /dev/null @@ -1,119 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "okta", - clients: [{ - clientId: "TODO", - clientSecret: "TODO" - }], - oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "okta", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - }, - }, - OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="okta", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - ), - ], - oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-saml.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-saml.mdx deleted file mode 100644 index f14c4732d3..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-saml.mdx +++ /dev/null @@ -1,126 +0,0 @@ - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "boxy-saml", - name: "", // Replace with the correct provider name - clients: [{ - clientId: "TODO", - clientSecret: "TODO", - additionalConfig: { - "boxyURL": "" - } - }], - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "boxy-saml", - Name: "", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "TODO:", - ClientSecret: "TODO:", - AdditionalConfig: map[string]interface{}{ - "boxyURL": "", - }, - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="boxy-saml", - name="", - clients=[ - ProviderClientConfig( - client_id="TODO:", - client_secret="TODO:", - additional_config={ - "boxyURL": "", - }, - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_blocks/single-tenant-provider-config-twitter.mdx b/docs/authentication/social/_blocks/single-tenant-provider-config-twitter.mdx deleted file mode 100644 index 848db81f66..0000000000 --- a/docs/authentication/social/_blocks/single-tenant-provider-config-twitter.mdx +++ /dev/null @@ -1,115 +0,0 @@ - - - - - -```tsx -import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty"; - -SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - { - config: { - thirdPartyId: "twitter", - clients: [{ - clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC" - }] - } - } - ] - } - }), // initializes signin / sign up features - ] -}); -``` - - - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/thirdparty" - "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - thirdparty.Init(&tpmodels.TypeInput{ - SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ - Providers: []tpmodels.ProviderInput{ - { - Config: tpmodels.ProviderConfig{ - ThirdPartyId: "twitter", - Clients: []tpmodels.ProviderClientConfig{ - { - ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - }, - }, - }, - }, - }, - }, - }), - }, - }) -} -``` - - - - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import thirdparty -from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - thirdparty.init( - - # highlight-start - sign_in_and_up_feature=SignInAndUpFeature( - providers=[ - ProviderInput( - config=ProviderConfig( - third_party_id="twitter", - clients=[ - ProviderClientConfig( - client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", - client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", - ), - ], - ), - ), - ] - ) - # highlight-end - ) - ] -) -``` - - - - diff --git a/docs/authentication/social/_category_.json b/docs/authentication/social/_category_.json deleted file mode 100644 index cc98313a23..0000000000 --- a/docs/authentication/social/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Social Login", - "position": 3 -} diff --git a/docs/authentication/social/add-multiple-clients-for-the-same-provider.mdx b/docs/authentication/social/add-multiple-clients-for-the-same-provider.mdx index 58d42deda6..a2cd05f7e5 100644 --- a/docs/authentication/social/add-multiple-clients-for-the-same-provider.mdx +++ b/docs/authentication/social/add-multiple-clients-for-the-same-provider.mdx @@ -1,18 +1,10 @@ --- title: Add multiple clients for the same provider -hide_title: true -sidebar_position: 6 -description: >- - Configure multiple client IDs for social login providers in web and mobile - applications. -page_type: guide -recipe: thirdparty -category: thirdparty +description: Configure multiple client IDs for social login providers in web and mobile applications. +sidebar: + order: 6 --- - -# Add multiple clients for the same provider - ## Overview If you use a third-party login method for your web and mobile app, then you might need to setup different Client ID/Secret for the same provider on the backend. @@ -21,8 +13,8 @@ For example, in case of Apple login, Apple gives you different client IDs for iO ## Before you start -This guide assumes that you have already implemented the [EmailPassword recipe](/docs/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +This guide assumes that you have already implemented the [EmailPassword recipe](/authentication/email-password/introduction) and have a working application integrated with **SuperTokens**. +If you have not, please check the [Quickstart Guide](/quickstart). ## Steps @@ -32,42 +24,41 @@ Add more clients to the Apple.init on the backend. Each client would need to be uniquely identified, and you achieve this using the `clientType` string. For example, you can add one `clientType` for `web-and-android` and one for `ios`. - - - - + + ```tsx import { ProviderInput } from "supertokens-node/recipe/thirdparty/types"; let providers: ProviderInput[] = [ - { - config: { - thirdPartyId: "apple", - clients: [{ - clientType: "web-and-android", - clientId: "...", - additionalConfig: { - "keyId": "...", - "privateKey": "...", - "teamId": "...", - } - }, { - clientType: "ios", - clientId: "...", - additionalConfig: { - "keyId": "...", - "privateKey": "...", - "teamId": "...", - } - }] - } - } -] + { + config: { + thirdPartyId: "apple", + clients: [ + { + clientType: "web-and-android", + clientId: "...", + additionalConfig: { + keyId: "...", + privateKey: "...", + teamId: "...", + }, + }, + { + clientType: "ios", + clientId: "...", + additionalConfig: { + keyId: "...", + privateKey: "...", + teamId: "...", + }, + }, + ], + }, + }, +]; ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" @@ -101,10 +92,8 @@ func main() { }} } ``` - - - - + + ```python from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig @@ -136,109 +125,108 @@ providers = [ ), ] ``` - - - + + ### 2. Update the frontend configuration Use the right `clientType` as shown below: - - - - - - - - - + + We pass in the `clientType` during the init call. + + +When making calls to the APIs from your mobile app, the request body also takes a `clientType` prop as seen in the above API calls. + + - - + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; +import SuperTokens from "supertokens-web-js"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - // highlight-next-line - clientType: "web-and-android", - recipeList: [/*...*/], + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + clientType: "web-and-android", + recipeList: [ + /*...*/ + ], }); ``` - - - + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." +supertokens.init({ + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + clientType: "web-and-android", + recipeList: [ + /*...*/ + ], +}); +``` + + + + + + + + + + + + If you are using the pre-built UI SDK (SuperTokens-auth-react) as well, you can provide the `clientType` configuration to it as follows: - - - + + + + + + + + + ```tsx -import SuperTokens from 'supertokens-auth-react'; +import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - // highlight-next-line - clientType: "web-and-android", - recipeList: [/*...*/], + appInfo: { + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + clientType: "web-and-android", + recipeList: [ + /*...*/ + ], }); ``` + + + + - - - - - - - -We pass in the `clientType` during the init call. - - - -```tsx -import supertokens from "supertokens-web-js-script"; -supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - // highlight-next-line - clientType: "web-and-android", - recipeList: [/*...*/], -}); -``` - - - - - - - - -When making calls to the APIs from your mobile app, the request body also takes a `clientType` prop as seen in the above API calls. - - - ## See also - - - - - + + + + + diff --git a/docs/authentication/social/built-in-providers-config.mdx b/docs/authentication/social/built-in-providers-config.mdx new file mode 100644 index 0000000000..e76dcb04f8 --- /dev/null +++ b/docs/authentication/social/built-in-providers-config.mdx @@ -0,0 +1,3118 @@ +--- +title: Built-in providers +description: Configure single or multi-tenant login/SSO with built-in provider settings. +sidebar: + order: 3 +--- + +This page shows a full list of all the built-in providers exposed by **SuperTokens**. + + + + + +## Google + +To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en). +Set the authorisation callback URL to `/auth/callback/google` + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="google", + clients=[ + ProviderClientConfig( + client_id="", + client_secret="", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## Google workspaces + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google-workspaces", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + additionalConfig: { + hd: "example.com", + }, + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google-workspaces", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + AdditionalConfig: map[string]interface{}{ + "hd": "example.com", + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="google-workspaces", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + additional_config={ + "hd": "example.com", + }, + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## Apple + +To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003). + +Initialize the ThirdParty and Session recipes on the frontend and backend. When using the prebuilt UI, add Apple to its frontend provider list. + +Unlike other providers, Apple sends a form POST to your backend callback instead of redirecting directly to the frontend. Set `redirectURIOnProviderDashboard` to a backend route such as `/auth/callback/apple`, and set `frontendRedirectURI` to the frontend callback page. The backend middleware redirects to that page, which completes authentication by calling `signInAndUp`. + +Apple doesn't allow `localhost` in the provider callback URL. +If you are in `dev` mode, you can use the `dev` keys provided above. + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "apple", + clients: [ + { + clientId: "", + additionalConfig: { + keyId: "", + privateKey: "", + teamId: "", + }, + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "apple", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "4398792-io.supertokens.example.service", + AdditionalConfig: map[string]interface{}{ + "keyId": "7M48Y4RYDL", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + "teamId": "YWQCXGJRJL", + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="apple", + clients=[ + ProviderClientConfig( + client_id="4398792-io.supertokens.example.service", + additional_config={ + "keyId": "7M48Y4RYDL", + "privateKey": "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgu8gXs+XYkqXD6Ala9Sf/iJXzhbwcoG5dMh1OonpdJUmgCgYIKoZIzj0DAQehRANCAASfrvlFbFCYqn3I2zeknYXLwtH30JuOKestDbSfZYxZNMqhF/OzdZFTV0zc5u5s3eN+oCWbnvl0hM+9IW0UlkdA\n-----END PRIVATE KEY-----", + "teamId": "YWQCXGJRJL", + }, + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## Discord + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "discord", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "discord", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="discord", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## Facebook + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "facebook", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "facebook", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="facebook", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## GitHub + +To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app). +Set the authorisation callback URL to `/auth/callback/github` + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "github", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "github", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="github", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## GitLab + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "gitlab", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "gitlab", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="gitlab", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" + ), + ), + ] + ) + ) + ] +) +``` + + + +## Twitter + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "twitter", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "twitter", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="twitter", + clients=[ + ProviderClientConfig( + client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## LinkedIn + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "linkedin", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "linkedin", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="linkedin", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +## Okta + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "okta", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "okta", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="okta", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", + ), + ), + ] + ) + ) + ] +) +``` + + + +## SAML + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "boxy-saml", + name: "", // Replace with the correct provider name + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + additionalConfig: { + boxyURL: "", + }, + }, + ], + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "boxy-saml", + Name: "", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + AdditionalConfig: map[string]interface{}{ + "boxyURL": "", + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="boxy-saml", + name="", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + additional_config={ + "boxyURL": "", + }, + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + + +## Active Directory + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "active-directory", + clients: [ + { + clientId: "TODO", + clientSecret: "TODO", + }, + ], + oidcDiscoveryEndpoint: + "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }, + }, + ], + }, + }), // initializes signin / sign up features + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "active-directory", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO:", + ClientSecret: "TODO:", + }, + }, + OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="active-directory", + clients=[ + ProviderClientConfig( + client_id="TODO:", + client_secret="TODO:", + ), + ], + oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + ), + ), + ] + ) + ) + ] +) +``` + + + + + + + + +Call the following function / API to add the third party provider to a specific tenant. + +## Google + +To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en) + + + +Create Google Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "google", + name: "Google", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Name: "Google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="google", + name="Google", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="google", + name="Google", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "google", + "name": "Google", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ] + } +}' +``` + + + + + + +## Google workspaces + + + +Create Google Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "google-workspaces", + name: "Google Workspaces", + clients: [ + { + clientId: "...", + clientSecret: "...", + additionalConfig: { + hd: "example.com", + }, + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "google-workspaces", + Name: "Google Workspaces", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + AdditionalConfig: map[string]interface{}{ + "hd": "example.com", + }, + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="google-workspaces", + name="Google Workspaces", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "hd": "example.com", + }, + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="google-workspaces", + name="Google Workspaces", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "hd": "example.com", + }, + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "google-workspaces", + "name": "Google Workspaces", + "clients": [ + { + "clientId": "...", + "clientSecret": "...", + "additionalConfig": { + "hd": "example.com" + } + } + ] + } +}' +``` + + + + + + +## Apple + +To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003) +Note that Apple doesn't allow `localhost` in the URL. +If you are in `dev` mode, you can use the `dev` keys provided above. + +Call the following function / API to add the third party provider to a specific tenant. + + + +Create Apple Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "apple", + name: "Apple", + clients: [ + { + clientId: "...", + additionalConfig: { + keyId: "...", + privateKey: "...", + teamId: "...", + }, + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "apple", + Name: "Apple", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + AdditionalConfig: map[string]interface{}{ + "keyId": "...", + "privateKey": "...", + "teamId": "...", + }, + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="apple", + name="Apple", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "keyId": "...", + "privateKey": "...", + "teamId": "...", + }, + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="apple", + name="Apple", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "keyId": "...", + "privateKey": "...", + "teamId": "...", + }, + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "apple", + "name": "Apple", + "clients": [ + { + "clientId": "...", + "additionalConfig": { + "keyId": "...", + "privateKey": "...", + "teamId": "..." + } + } + ] + } +}' +``` + + + + + + +## Discord + + + +Create Discord Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "discord", + name: "Discord", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "discord", + Name: "Discord", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="discord", + name="Discord", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="discord", + name="Discord", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "discord", + "name": "Discord", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ] + } +}' +``` + + + + + + + +## Facebook + + + +Create Facebook Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "facebook", + name: "Facebook", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "facebook", + Name: "Facebook", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="facebook", + name="Facebook", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="facebook", + name="Facebook", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "facebook", + "name": "Facebook", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ] + } +}' +``` + + + + + + + +## GitHub + +To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app) + + + +Create GitHub Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "github", + name: "GitHub", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "github", + Name: "GitHub", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="github", + name="GitHub", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="github", + name="GitHub", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '/recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "github", + "name": "GitHub", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ] + } +}' +``` + + + + + + + +## GitLab + + + +Create GitLab Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "gitlab", + name: "GitLab", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + oidcDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "gitlab", + Name: "GitLab", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + OIDCDiscoveryEndpoint: "https://gitlab.example.com/.well-known/openid-configuration", + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="gitlab", + name="Gitlab", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="gitlab", + name="Gitlab", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://gitlab.example.com/.well-known/openid-configuration" +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "gitlab", + "name": "GitLab", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ], + "oidcDiscoveryEndpoint": "https://gitlab.example.com/.well-known/openid-configuration" + } +}' +``` + + + + + + + +## Twitter + + + +Create Twitter Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "twitter", + name: "Twitter", + clients: [ + { + clientId: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + clientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "twitter", + Name: "Twitter", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + ClientSecret: "BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="twitter", + name="Twitter", + clients=[ + ProviderClientConfig( + client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="twitter", + name="Twitter", + clients=[ + ProviderClientConfig( + client_id="4398792-WXpqVXRiazdRMGNJdEZIa3RVQXc6MTpjaQ", + client_secret="BivMbtwmcygbRLNQ0zk45yxvW246tnYnTFFq-LH39NwZMxFpdC", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "twitter", + "name": "Twitter", + "clients": [ + { + "clientId": " ", + "clientSecret": "" + } + ] + } +}' +``` + + + + + + + +## LinkedIn + + + +Create LinkedIn Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "linkedin", + name: "LinkedIn", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "linkedin", + Name: "LinkedIn", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="linkedin", + name="LinkedIn", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="linkedin", + name="LinkedIn", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "linkedin", + "name": "LinkedIn", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ] + } +}' +``` + + + + + + + +## Okta + + + +Create Okta Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "okta", + name: "Okta", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + oidcDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "okta", + Name: "Okta", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + OIDCDiscoveryEndpoint: "https://dev-.okta.com/.well-known/openid-configuration", + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="okta", + name="Okta", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="okta", + name="Okta", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://dev-.okta.com/.well-known/openid-configuration", +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "okta", + "name": "Okta", + "clients": [ + { + "clientId": "...", + "clientSecret": "..." + } + ], + "oidcDiscoveryEndpoint": "https://dev-.okta.com/.well-known/openid-configuration" + } +}' +``` + + + + + + + +## SAML + + + +Create SAML Provider + + + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "boxy-saml", + name: "", + clients: [ + { + clientId: "...", + clientSecret: "...", + additionalConfig: { + boxyURL: "", + }, + }, + ], + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "boxy-saml", + Name: "", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + AdditionalConfig: map[string]interface{}{ + "boxyURL": "", + }, + }, + }, + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="boxy-saml", + name="", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "boxyURL": "", + }, + ), + ], + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + +```python +from supertokens_python.recipe.multitenancy.syncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +tenant_id = "customer1" +result = create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="boxy-saml", + name="", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + additional_config={ + "boxyURL": "", + }, + ), + ], +)) + +if result.status != "OK": + print("handle error") +elif result.created_new: + print("Provider added to customer1") +else: + print("Existing provider config overwritten for customer1") +``` + + + + +```bash +curl --location --request PUT '//recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "config": { + "thirdPartyId": "boxy-SAML", + "name": "", + "clients": [ + { + "clientId": "...", + "clientSecret": "...", + "additionalConfig": { + "boxyURL": "" + } + } + ] + } +}' +``` + + + + + + +## Active Directory + + + +```tsx +import Multitenancy from "supertokens-node/recipe/multitenancy"; + +async function addThirdPartyToTenant() { + let resp = await Multitenancy.createOrUpdateThirdPartyConfig("customer1", { + thirdPartyId: "active-directory", + name: "Active Directory", + clients: [ + { + clientId: "...", + clientSecret: "...", + }, + ], + oidcDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }); + + if (resp.createdNew) { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/multitenancy" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + tenantId := "customer1" + + resp, err := multitenancy.CreateOrUpdateThirdPartyConfig(tenantId, tpmodels.ProviderConfig{ + ThirdPartyId: "active-directory", + Name: "Active Directory", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "...", + ClientSecret: "...", + }, + }, + OIDCDiscoveryEndpoint: "https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + }, nil) + + if err != nil { + // handle error + } + if resp.OK.CreatedNew { + // Provider added to customer1 + } else { + // Existing provider config overwritten for customer1 + } +} +``` + + +```python +from supertokens_python.recipe.multitenancy.asyncio import create_or_update_third_party_config +from supertokens_python.recipe.thirdparty.provider import ProviderConfig, ProviderClientConfig + +async def some_func(): + tenant_id = "customer1" + result = await create_or_update_third_party_config(tenant_id, ProviderConfig( + third_party_id="active-directory", + name="Active Directoy", + clients=[ + ProviderClientConfig( + client_id="...", + client_secret="...", + ), + ], + oidc_discovery_endpoint="https://login.microsoftonline.com//v2.0/.well-known/openid-configuration", + )) + + if result.status != "OK": + print("handle error") + elif result.created_new: + print("Provider added to customer1") + else: + print("Existing provider config overwritten for customer1") +``` + + + + + + + +--- + +## See also + + + + + + + diff --git a/docs/authentication/social/built-in-providers.mdx b/docs/authentication/social/built-in-providers.mdx deleted file mode 100644 index 36e1cc93e9..0000000000 --- a/docs/authentication/social/built-in-providers.mdx +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: built-in-providers-config -title: Built-in providers -hide_title: true -sidebar_position: 3 -description: Configure single or multi-tenant login/SSO with built-in provider settings. -page_type: guide -recipe: thirdparty -category: thirdparty ---- - -import SingleTenantProviderConfigGoogle from "./_blocks/single-tenant-provider-config-google.mdx"; -import SingleTenantProviderConfigGoogleWorkspaces from "./_blocks/single-tenant-provider-config-google-workspaces.mdx"; -import SingleTenantProviderConfigApple from "./_blocks/single-tenant-provider-config-apple.mdx"; -import SingleTenantProviderConfigDiscord from "./_blocks/single-tenant-provider-config-discord.mdx"; -import SingleTenantProviderConfigFacebook from "./_blocks/single-tenant-provider-config-facebook.mdx"; -import SingleTenantProviderConfigLinkedIn from "./_blocks/single-tenant-provider-config-linkedin.mdx"; -import SingleTenantProviderConfigOkta from "./_blocks/single-tenant-provider-config-okta.mdx"; -import SingleTenantProviderConfigGithub from "./_blocks/single-tenant-provider-config-github.mdx"; -import SingleTenantProviderConfigTwitter from "./_blocks/single-tenant-provider-config-twitter.mdx"; -import SingleTenantProviderConfigGitlab from "./_blocks/single-tenant-provider-config-gitlab.mdx"; -import SingleTenantProviderConfigSaml from "./_blocks/single-tenant-provider-config-saml.mdx"; -import SingleTenantProviderConfigActiveDirectory from "./_blocks/single-tenant-provider-config-active-directory.mdx"; -import MultiTenantProviderConfigGoogle from "./_blocks/multi-tenant-provider-config-google.mdx"; -import MultiTenantProviderConfigGoogleWorkspaces from "./_blocks/multi-tenant-provider-config-google-workspaces.mdx"; -import MultiTenantProviderConfigApple from "./_blocks/multi-tenant-provider-config-apple.mdx"; -import MultiTenantProviderConfigDiscord from "./_blocks/multi-tenant-provider-config-discord.mdx"; -import MultiTenantProviderConfigFacebook from "./_blocks/multi-tenant-provider-config-facebook.mdx"; -import MultiTenantProviderConfigLinkedIn from "./_blocks/multi-tenant-provider-config-linkedin.mdx"; -import MultiTenantProviderConfigOkta from "./_blocks/multi-tenant-provider-config-okta.mdx"; -import MultiTenantProviderConfigGithub from "./_blocks/multi-tenant-provider-config-github.mdx"; -import MultiTenantProviderConfigTwitter from "./_blocks/multi-tenant-provider-config-twitter.mdx"; -import MultiTenantProviderConfigGitlab from "./_blocks/multi-tenant-provider-config-gitlab.mdx"; -import MultiTenantProviderConfigSaml from "./_blocks/multi-tenant-provider-config-saml.mdx"; -import MultiTenantProviderConfigActiveDirectory from "./_blocks/multi-tenant-provider-config-active-directory.mdx"; - - - - -# Built-in providers - -This page shows a full list of all the built-in providers exposed by **SuperTokens**. - - - - - -## Google - -To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en). -Set the authorisation callback URL to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/callback/google` - - - -## Google workspaces - - - -## Apple - -To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003). -Set the authorisation callback URL to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/callback/apple`. -Note that Apple doesn't allow `localhost` in the URL. -If you are in `dev` mode, you can use the `dev` keys provided above. - - - -## Discord - - - -## Facebook - - - -## GitHub - -To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app). -Set the authorisation callback URL to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/callback/github` - - - -## GitLab - - - -## Twitter - - - -## LinkedIn - - - -## Okta - - - -## SAML - - - - -## Active Directory - - - - - - - - -Call the following function / API to add the third party provider to a specific tenant. - -## Google - -To generate your client ID and secret follow the [official documentation](https://support.google.com/cloud/answer/6158849?hl=en) - - - -## Google workspaces - - - -## Apple - -To generate your client ID and secret follow [this article](https://medium.com/identity-beyond-borders/how-to-configure-sign-in-with-apple-77c61e336003) -Note that Apple doesn't allow `localhost` in the URL. -If you are in `dev` mode, you can use the `dev` keys provided above. - - - -## Discord - - - - -## Facebook - - - - -## GitHub - -To generate your client ID and secret follow the [official documentation](https://docs.github.com/en/developers/apps/creating-an-oauth-app) - - - - -## GitLab - - - - -## Twitter - - - - -## LinkedIn - - - - -## Okta - - - - -## SAML - - - -## Active Directory - - - - - - - ---- - -## See also - - - - - - - diff --git a/docs/authentication/social/custom-invite-flow.mdx b/docs/authentication/social/custom-invite-flow.mdx index abf6d26cbd..02b525f05f 100644 --- a/docs/authentication/social/custom-invite-flow.mdx +++ b/docs/authentication/social/custom-invite-flow.mdx @@ -1,97 +1,105 @@ --- title: Implement a custom invite flow -hide_title: true -sidebar_position: 8 -description: >- - Implement an allow list for third-party sign-ups using the SuperTokens user - metadata recipe. -page_type: guide -recipe: thirdparty -category: thirdparty +description: Implement a list of approved email addresses for third-party sign-ups using the SuperTokens user metadata recipe. +sidebar: + order: 8 --- - -# Implement a custom invite flow - ## Overview -This guide shows you how to disable public sign ups to allow only certain people to access your app. -From a third-party login perspective, you need to maintain an allow list of emails and validate the users based on it. +This guide shows you how to disable public sign-ups to allow only certain people to access your app. +For third-party login, maintain a list of approved email addresses and validate users against it. ## Before you start The tutorial assumes that you already have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). +If you have not, please check the [Quickstart Guide](/quickstart). ### Prerequisites -This guide uses the `UserMetadata` recipe to store the allow list. -You need to [enable it](/docs/post-authentication/user-management/user-metadata) in the SDK initialization step. +This guide uses the `UserMetadata` recipe to store the list of approved email addresses. +You need to [enable it](/post-authentication/user-management/user-metadata) in the SDK initialization step. ## Steps -### 1. Implement the allow list - -You can store this list in your own database, or use the metadata feature provided by SuperTokens to store this. -This may seem like a strange use case of the user metadata recipe provided, but it works. +### 1. Implement the approved email list -The following code samples show you how to save the allow list in the user metadata. +You can store this list in your own database or use the metadata feature provided by SuperTokens. - - +The following code samples show you how to save the approved email list in user metadata. + + ```tsx -import UserMetadata from "supertokens-node/recipe/usermetadata" - -async function addEmailToAllowlist(email: string) { - let existingData = await UserMetadata.getUserMetadata("emailAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - allowList = [...allowList, email]; - await UserMetadata.updateUserMetadata("emailAllowList", { - allowList - }); +import UserMetadata from "supertokens-node/recipe/usermetadata"; + +function allowlistKey(tenantId: string) { + return `emailAllowList:${tenantId}`; } -async function isEmailAllowed(email: string) { - let existingData = await UserMetadata.getUserMetadata("emailAllowList"); - let allowList: string[] = existingData.metadata.allowList || []; - return allowList.includes(email); +async function addEmailToAllowlist(tenantId: string, email: string) { + let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId)); + let allowList: string[] = existingData.metadata.allowList || []; + allowList = [...allowList, email]; + await UserMetadata.updateUserMetadata(allowlistKey(tenantId), { + allowList, + }); +} + +async function isEmailAllowed(tenantId: string, email: string) { + let existingData = await UserMetadata.getUserMetadata(allowlistKey(tenantId)); + let allowList: string[] = existingData.metadata.allowList || []; + return allowList.includes(email); } ``` + + +```go +import ( + "fmt" - - + "github.com/supertokens/supertokens-golang/recipe/usermetadata" +) -```go -import "github.com/supertokens/supertokens-golang/recipe/usermetadata" +func allowlistKey(tenantID string) string { + return fmt.Sprintf("emailAllowList:%s", tenantID) +} + +func getAllowList(metadata map[string]interface{}) []string { + allowList := []string{} + rawAllowList, ok := metadata["allowList"].([]interface{}) + if !ok { + return allowList + } -func addEmailToAllowlist(email string) error { - existingData, err := usermetadata.GetUserMetadata("emailAllowList") + for _, value := range rawAllowList { + email, ok := value.(string) + if ok { + allowList = append(allowList, email) + } + } + return allowList +} + +func addEmailToAllowlist(tenantID string, email string) error { + existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID)) if err != nil { return err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata - } + allowList := getAllowList(existingData) allowList = append(allowList, email) - _, err = usermetadata.UpdateUserMetadata("emailAllowList", map[string]interface{}{ + _, err = usermetadata.UpdateUserMetadata(allowlistKey(tenantID), map[string]interface{}{ "allowList": allowList, }) return err } -func isEmailAllowed(email string) (bool, error) { - existingData, err := usermetadata.GetUserMetadata("emailAllowList") +func isEmailAllowed(tenantID string, email string) (bool, error) { + existingData, err := usermetadata.GetUserMetadata(allowlistKey(tenantID)) if err != nil { return false, err } - allowList := []string{} - allowListFromMetadata, ok := existingData["allowList"].([]string) - if ok { - allowList = allowListFromMetadata - } + allowList := getAllowList(existingData) for _, allowedEmail := range allowList { if allowedEmail == email { return true, nil @@ -100,10 +108,8 @@ func isEmailAllowed(email string) (bool, error) { return false, nil } ``` - - - - + + ```python from typing import List @@ -113,89 +119,118 @@ from supertokens_python.recipe.usermetadata.asyncio import ( ) -async def add_email_to_allow_list(email: str): - metadataResult = await get_user_metadata("emailAllowList") +def allowlist_key(tenant_id: str): + return f"emailAllowList:{tenant_id}" + + +async def add_email_to_allow_list(tenant_id: str, email: str): + metadataResult = await get_user_metadata(allowlist_key(tenant_id)) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] allow_list.append(email) - await update_user_metadata("emailAllowList", { + await update_user_metadata(allowlist_key(tenant_id), { "allowList": allow_list }) -async def is_email_allowed(email: str): - metadataResult = await get_user_metadata("emailAllowList") +async def is_email_allowed(tenant_id: str, email: str): + metadataResult = await get_user_metadata(allowlist_key(tenant_id)) allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else [] return email in allow_list ``` + + - - - -:::info Multi Tenancy -For a multi tenant setup, you can even store an allow list per tenant. -This setup allows limiting sign ups for different emails for different tenants. -If doing this, pass in the tenantID to the functions above, which you can obtain from the input to the API overrides shown below. -::: ### 2. Check if the email is allowed -Update the backend SDK API function to only allow sign up requests from users that are on the allow list. -To do this you need to use the check functions from the previous code snippet. +Update the backend SDK API function to only allow sign-up requests from users whose email addresses are on the approved list. +Use the check functions from the previous code snippet. - - +The overrides reject a provider response without an email before the SDK can generate a synthetic email for a provider +configured not to require one. -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." import ThirdParty from "supertokens-node/recipe/thirdparty"; import supertokens from "supertokens-node"; -declare let isEmailAllowed: (email: string) => Promise // REMOVE_FROM_OUTPUT + +class SignUpNotAllowedError extends Error {} ThirdParty.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signInUp: async function (input) { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - email: input.email - }); - if (existingUsers.length === 0) { - // this means that the email is new and is a sign up - if (!(await isEmailAllowed(input.email))) { - // email is not in allow list, so we disallow - throw new Error("No sign up") - } - } - // We allow the sign in / up operation - return originalImplementation.signInUp(input); - } + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signInUp: async function (input) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + thirdParty: { + id: input.thirdPartyId, + userId: input.thirdPartyUserId, + }, + }); + if (existingUsers.length === 0) { + if (!input.isVerified) { + throw new SignUpNotAllowedError(); } + if (!(await isEmailAllowed(input.tenantId, input.email))) { + throw new SignUpNotAllowedError(); + } + } + return originalImplementation.signInUp(input); }, - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInUpPOST: async function (input) { - try { - return await originalImplementation.signInUpPOST!(input); - } catch (err: any) { - if (err.message === "No sign up") { - // this error was thrown from our function override above. - // so we send a useful message to the user - return { - status: "GENERAL_ERROR", - message: "Sign ups are disabled. Please contact the admin." - } - } - throw err; - } - } + }; + }, + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInUpPOST: async function (input) { + try { + const provider = input.provider; + const providerWithEmailCheck = + provider.type === "oauth2" + ? { + ...provider, + getUserInfo: async (getUserInfoInput: Parameters[0]) => { + const userInfo = await provider.getUserInfo(getUserInfoInput); + if (userInfo.email === undefined) { + throw new SignUpNotAllowedError(); + } + return userInfo; + }, + } + : { + ...provider, + getUserInfo: async (getUserInfoInput: Parameters[0]) => { + const userInfo = await provider.getUserInfo(getUserInfoInput); + if (userInfo.email === undefined) { + throw new SignUpNotAllowedError(); + } + return userInfo; + }, + }; + + return await originalImplementation.signInUpPOST!({ + ...input, + provider: providerWithEmailCheck, + }); + } catch (err: unknown) { + if (err instanceof SignUpNotAllowedError) { + return { + status: "GENERAL_ERROR", + message: "Sign-ups are disabled. Please contact the admin.", + }; } - } - } -}) + throw err; + } + }, + }; + }, + }, +}); ``` - - - + + +Pass the `isEmailAllowed` helper from the previous step to `initThirdPartyWithInvites`, and include the returned recipe in your SuperTokens `RecipeList`. Add your provider configuration to the `TypeInput` below. ```go import ( @@ -206,53 +241,55 @@ import ( "github.com/supertokens/supertokens-golang/supertokens" ) -func isEmailAllowed(email string) (bool, error) { - // ... from previous code snippet - return false, nil -} +var errSignUpNotAllowed = errors.New("sign up not allowed") -func main() { - thirdparty.Init(&tpmodels.TypeInput{ +func initThirdPartyWithInvites(isEmailAllowed func(tenantID, email string) (bool, error)) supertokens.Recipe { + return thirdparty.Init(&tpmodels.TypeInput{ Override: &tpmodels.OverrideStruct{ - Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { - ogThirdPartySignInUp := *originalImplementation.SignInUp - - (*originalImplementation.SignInUp) = func(thirdPartyID, thirdPartyUserID, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext supertokens.UserContext) (tpmodels.SignInUpResponse, error) { - existingUsers, err := thirdparty.GetUsersByEmail(tenantId, email) - if err != nil { - return tpmodels.SignInUpResponse{}, err - } + APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { + originalSignInUpPOST := *originalImplementation.SignInUpPOST - if len(existingUsers) == 0 { - // this means that the email is new and is a sign up - allowed, err := isEmailAllowed(email) + (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) { + providerWithInviteCheck := *provider + originalGetUserInfo := provider.GetUserInfo + providerWithInviteCheck.GetUserInfo = func(oAuthTokens tpmodels.TypeOAuthTokens, userContext supertokens.UserContext) (tpmodels.TypeUserInfo, error) { + userInfo, err := originalGetUserInfo(oAuthTokens, userContext) if err != nil { - return tpmodels.SignInUpResponse{}, err + return tpmodels.TypeUserInfo{}, err } - if !allowed { - return tpmodels.SignInUpResponse{}, errors.New("No sign up") + + if userInfo.Email == nil { + return tpmodels.TypeUserInfo{}, errSignUpNotAllowed } - } - // We allow the sign in / up operation - return ogThirdPartySignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext) - } - return originalImplementation - }, + existingUser, err := thirdparty.GetUserByThirdPartyInfo(tenantId, provider.ID, userInfo.ThirdPartyUserId, userContext) + if err != nil { + return tpmodels.TypeUserInfo{}, err + } - APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { - originalSignInUpPOST := *originalImplementation.SignInUpPOST + if existingUser == nil { + if !userInfo.Email.IsVerified { + return tpmodels.TypeUserInfo{}, errSignUpNotAllowed + } + + allowed, err := isEmailAllowed(tenantId, userInfo.Email.ID) + if err != nil { + return tpmodels.TypeUserInfo{}, err + } + if !allowed { + return tpmodels.TypeUserInfo{}, errSignUpNotAllowed + } + } - (*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) { + return userInfo, nil + } - resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext) + resp, err := originalSignInUpPOST(&providerWithInviteCheck, input, tenantId, options, userContext) - if err.Error() == "No sign up" { - // this error was thrown from our function override above. - // so we send a useful message to the user + if errors.Is(err, errSignUpNotAllowed) { return tpmodels.SignInUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ - Message: "Sign ups are disabled. Please contact the admin.", + Message: "Sign-ups are disabled. Please contact the admin.", }, }, nil } @@ -266,11 +303,10 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from copy import copy from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init @@ -283,16 +319,24 @@ from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, ) from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo -from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider +from supertokens_python.recipe.thirdparty.types import ( + RawUserInfoFromProvider, + ThirdPartyInfo, + UserInfo, +) from supertokens_python.types import GeneralErrorResponse from supertokens_python.types.base import AccountInfoInput -async def is_email_allowed(email: str): +async def is_email_allowed(tenant_id: str, email: str): # from previous code snippet.. return False +class SignUpNotAllowedError(Exception): + pass + + def override_thirdparty_functions(original_implementation: RecipeInterface): original_thirdparty_sign_in_up = original_implementation.sign_in_up @@ -309,13 +353,20 @@ def override_thirdparty_functions(original_implementation: RecipeInterface): user_context: Dict[str, Any], ): existing_users = await list_users_by_account_info( - tenant_id, AccountInfoInput(email=email) + tenant_id, + AccountInfoInput( + third_party=ThirdPartyInfo( + third_party_user_id=third_party_user_id, + third_party_id=third_party_id, + ) + ), ) if len(existing_users) == 0: - if not await is_email_allowed(email): - raise Exception("No sign up") + if not is_verified: + raise SignUpNotAllowedError() + if not await is_email_allowed(tenant_id, email): + raise SignUpNotAllowedError() - # this means this email is new so we allow sign up return await original_thirdparty_sign_in_up( third_party_id, third_party_user_id, @@ -328,7 +379,6 @@ def override_thirdparty_functions(original_implementation: RecipeInterface): tenant_id, user_context, ) - raise Exception("No sign up") original_implementation.sign_in_up = thirdparty_sign_in_up @@ -348,9 +398,26 @@ def override_thirdparty_apis(original_implementation: APIInterface): api_options: APIOptions, user_context: Dict[str, Any], ): + provider_with_email_check = copy(provider) + original_get_user_info = provider.get_user_info + + async def get_user_info_with_email_check( + oauth_tokens: Dict[str, Any], user_context: Dict[str, Any] + ) -> UserInfo: + user_info = await original_get_user_info(oauth_tokens, user_context) + if user_info.email is None: + raise SignUpNotAllowedError() + return user_info + + setattr( + provider_with_email_check, + "get_user_info", + get_user_info_with_email_check, + ) + try: return await original_sign_in_up_post( - provider, + provider_with_email_check, redirect_uri_info, oauth_tokens, session, @@ -359,12 +426,10 @@ def override_thirdparty_apis(original_implementation: APIInterface): api_options, user_context, ) - except Exception as e: - if str(e) == "No sign up": - return GeneralErrorResponse( - "Seems like you already have an account with another method. Please use that instead." - ) - raise e + except SignUpNotAllowedError: + return GeneralErrorResponse( + "Sign-ups are disabled. Please contact the admin." + ) original_implementation.sign_in_up_post = thirdparty_sign_in_up_post return original_implementation @@ -372,7 +437,7 @@ def override_thirdparty_apis(original_implementation: APIInterface): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( @@ -382,17 +447,15 @@ init( ], ) ``` - - - - + + ## See also - - - - - - + + + + + + diff --git a/docs/authentication/social/custom-providers.mdx b/docs/authentication/social/custom-providers.mdx index 598f4df6c8..54e29367c3 100644 --- a/docs/authentication/social/custom-providers.mdx +++ b/docs/authentication/social/custom-providers.mdx @@ -1,18 +1,15 @@ --- title: Custom providers -sidebar_position: 4 description: 'Add custom authentication providers to SuperTokens ' -page_type: guide -recipe: thirdparty -category: thirdparty +sidebar: + order: 4 --- +## Overview -## Overview +If you can't find a provider in [the built-in list](/authentication/social/built-in-providers-config), you can add your own custom implementation as shown below. -If you can't find a provider in [the built-in list](/docs/authentication/social/built-in-providers-config), you can add your own custom implementation as shown below. - -:::info Note +:::info[Note] If you think that SuperTokens should support this provider by default, make sure to let the team know [on GitHub](https://github.com/supertokens/supertokens-node/issues/88). ::: @@ -22,149 +19,168 @@ If you think that SuperTokens should support this provider by default, make sure ### 1. Render the authentication method in the authentication UI - - - + + - - + + Include the provider in the `providers` array in the frontend SDK. + + +:::warning[This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form.] +::: + + +:::warning[This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form.] +::: + + + + ```tsx import React from "react"; import SuperTokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - // highlight-start - providers: [ - { - id: "custom", - name: "X", // Will display "Continue with X" - - // optional - // you do not need to add a click handler to this as - // we add it for you automatically. - buttonComponent: (props: {name: string}) =>
{"Login with " + props.name}
- } - ], - // highlight-end - // ... - }, - // ... - }), + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + id: "custom", + name: "X", // Will display "Continue with X" + + // optional + // you do not need to add a click handler to this as + // we add it for you automatically. + buttonComponent: (props: { name: string }) => ( +
+ {"Login with " + props.name} +
+ ), + }, + ], // ... - ] + }, + // ... + }), + // ... + ], }); ``` -
+ + - + + -:::caution -This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form. -::: + + - +
- + -:::caution -This is impossible for non-react apps at the moment. Please use custom UI instead for the sign in form. -::: +You need to build your own UI listing the buttons for each of the social login providers you want your users to use. See [the implementation details page](/authentication/social/initial-setup#2-add-the-login-ui) for what to do after a user clicks one of the buttons. - - + -
+### 2. Configure the credentials - -You need to build your own UI listing the buttons for each of the social login providers you want your users to use. See [the implementation details page](../../custom-ui/thirdparty-login) for what to do after a user clicks one of the buttons. + - + -### 2. Configure the credentials +You can define a custom provider in a couple of ways. The simplest method is to provide the configuration for the `AuthorizationEndpoint`, `TokenEndpoint`, and the mapping for how the user's ID and email from the provider's profile information endpoint. This appears below: + + +Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section - - - +Social/Enterprise providers -You can define a custom provider in a couple of ways. The simplest method is to provide the configuration for the `AuthorizationEndpoint`, `TokenEndpoint`, and the mapping for how the user's ID and email from the provider's profile information endpoint. This appears below: +Select **Add Custom Provider** option - +New Provider - +Fill in the details as shown below and click on **Save** + +OAuth2 provider + + + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - // highlight-start - providers: [{ - config: { - thirdPartyId: "custom", - name: "Custom provider", - clients: [{ - clientId: "...", - clientSecret: "...", - scope: ["profile", "email"] - }], - authorizationEndpoint: "https://example.com/oauth/authorize", - authorizationEndpointQueryParams: { - "someKey1": "value1", - "someKey2": null - }, - tokenEndpoint: "https://example.com/oauth/token", - tokenEndpointBodyParams: { - "someKey1": "value1", - }, - userInfoEndpoint: "https://example.com/oauth/userinfo", - userInfoMap: { - fromUserInfoAPI: { - userId: "id", - email: "email", - emailVerified: "email_verified", - } - } - } - }] - // highlight-end - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + name: "Custom provider", + clients: [ + { + clientId: "...", + clientSecret: "...", + scope: ["profile", "email"], + }, + ], + authorizationEndpoint: "https://example.com/oauth/authorize", + authorizationEndpointQueryParams: { + someKey1: "value1", + someKey2: null, + }, + tokenEndpoint: "https://example.com/oauth/token", + tokenEndpointBodyParams: { + someKey1: "value1", + }, + userInfoEndpoint: "https://example.com/oauth/userinfo", + userInfoMap: { + fromUserInfoAPI: { + userId: "id", + email: "email", + emailVerified: "email_verified", + }, + }, + }, + }, + ], + }, + }), + ], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty" @@ -179,7 +195,6 @@ func main() { SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ Providers: []tpmodels.ProviderInput{ { - // highlight-start Config: tpmodels.ProviderConfig{ ThirdPartyId: "custom", Name: "Custom provider", @@ -212,7 +227,6 @@ func main() { }, }, }, - // highlight-end }, }, }, @@ -221,11 +235,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature @@ -233,11 +245,10 @@ from supertokens_python.recipe.thirdparty.provider import UserInfoMap, UserField init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ thirdparty.init( - # highlight-start sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( @@ -273,19 +284,15 @@ init( ), ] ) - # highlight-end ) ] ) ``` - - - - - + + ```bash -curl --location --request PUT '^{coreInfo.uri}/public/recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/public/recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { @@ -316,26 +323,11 @@ curl --location --request PUT '^{coreInfo.uri}/public/recipe/multitenancy/config } }' ``` + + - - - - -Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section - -Social/Enterprise providers - -Select **Add Custom Provider** option - -New Provider - -Fill in the details as shown below and click on **Save** - -OAuth2 provider - - - - + + | Configuration Field | Description | Example | |---|---|---| @@ -347,67 +339,80 @@ Fill in the details as shown below and click on **Save** | `TokenEndpoint` | API endpoint for exchanging Authorization Code | Google: `"https://oauth2.googleapis.com/token"` | | `TokenEndpointBodyParams` | Optional configuration to modify request body | Can add, modify, or remove (using null) body params | | `UserInfoEndpoint` | API endpoint for getting user information | Google: `"https://www.googleapis.com/oauth2/v1/userinfo"` | -| `UserInfoMap.FromUserInfoAPI` | Maps provider's JSON response fields to user info | Example mapping:
`userId: "id"`
`email: "email"`
`emailVerified: "email_verified"`
For nested values use: `userId: "user.id"` | +| `UserInfoMap.FromUserInfoAPI` | Maps provider's JSON response fields to user info | Example mapping:
`userId: "id"`
`email: "email"`
`emailVerified: "email_verified"`
For nested values use: `userId: "user.id"` | - - +
+ If the provider is Open ID Connect (OIDC) compatible, you can provide URL for the `OIDCDiscoverEndpoint` configuration. The backend SDK automatically discovers authorization endpoint, token endpoint and the user info endpoint by querying the `/.well-known/openid-configuration`. Below is an example of how to set the OIDC discovery endpoint: - - + + +Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section + +Social/Enterprise providers + +Select **Add Custom Provider** option + +New Provider + +Fill in the details as shown below and click on **Save** + +OAuth2 provider + + + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - // highlight-start - providers: [{ - config: { - thirdPartyId: "custom", - name: "Custom provider", - clients: [{ - clientId: "...", - clientSecret: "...", - scope: ["profile", "email"] - }], - // highlight-start - oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", - // highlight-end - authorizationEndpointQueryParams: { - "someKey1": "value1", - "someKey2": null - }, - userInfoMap: { - fromIdTokenPayload: { - userId: "id", - email: "email", - emailVerified: "email_verified", - } - } - } - }] - // highlight-end - } - }) - ] -}) + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + name: "Custom provider", + clients: [ + { + clientId: "...", + clientSecret: "...", + scope: ["profile", "email"], + }, + ], + oidcDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", + authorizationEndpointQueryParams: { + someKey1: "value1", + someKey2: null, + }, + userInfoMap: { + fromIdTokenPayload: { + userId: "id", + email: "email", + emailVerified: "email_verified", + }, + }, + }, + }, + ], + }, + }), + ], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty" @@ -432,9 +437,7 @@ func main() { Scope: []string{"profile", "email"}, }, }, - // highlight-start OIDCDiscoveryEndpoint: "https://example.com/.well-known/openid-configuration", - // highlight-end AuthorizationEndpointQueryParams: map[string]interface{}{ // optional "someKey1": "value1", "someKey2": nil, @@ -459,11 +462,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature @@ -471,11 +472,10 @@ from supertokens_python.recipe.thirdparty.provider import UserInfoMap, UserField init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ thirdparty.init( - # highlight-start sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( @@ -506,20 +506,15 @@ init( ), ] ) - # highlight-end ) ] ) ``` - - - - - - + + ```bash -curl --location --request PUT '^{coreInfo.uri}/public/recipe/multitenancy/config/thirdparty' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/public/recipe/multitenancy/config/thirdparty' \ +--header 'api-key: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "config": { @@ -545,32 +540,17 @@ curl --location --request PUT '^{coreInfo.uri}/public/recipe/multitenancy/config } }' ``` + + - - - - -Select the **public** tenant from the tenant management page and then click on **Add new provider** in the Social/Enterprise Providers section - -Social/Enterprise providers - -Select **Add Custom Provider** option - -New Provider - -Fill in the details as shown below and click on **Save** - -OAuth2 provider - - - - + + - The configuration values are similar to the ones in the "Via OAuth endpoints" method. Please read that section to understand the `thirdPartyId`, `name`, `clients` configuration. - Unlike the "Via OAuth endpoints", you can obtain the user's info from the ID token payload using the configuration specified by you in the `UserInfoMap.FromIdTokenPayload` map. - You can also add the `UserInfoMap.FromUserInfoAPI` map as done in the "Via OAuth endpoints" section. SuperTokens auto merges the user information. - + --- @@ -581,73 +561,72 @@ Sometimes, one of the steps in the providers interaction may not be per a standa For example, the API call made to get the user's profile info makes a `GET` call to the `UserInfoEndpoint` with the user's access token. If your provider requires a different method or requires multiple calls to different endpoints to get the profile info, then you can override the default implementation as shown below: - - - + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [{ - config: { - thirdPartyId: "custom", - name: "Custom provider", - clients: [{ - clientId: "...", - clientSecret: "...", - scope: ["profile", "email"] - }], - authorizationEndpoint: "https://example.com/oauth/authorize", - authorizationEndpointQueryParams: { - "response_type": "token", // Changing an existing parameter - "response_mode": "form", // Adding a new parameter - "scope": null, // Removing a parameter - }, - tokenEndpoint: "https://example.com/oauth/token" + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + name: "Custom provider", + clients: [ + { + clientId: "...", + clientSecret: "...", + scope: ["profile", "email"], + }, + ], + authorizationEndpoint: "https://example.com/oauth/authorize", + authorizationEndpointQueryParams: { + response_type: "token", // Changing an existing parameter + response_mode: "form", // Adding a new parameter + scope: null, // Removing a parameter + }, + tokenEndpoint: "https://example.com/oauth/token", + }, + override: (originalImplementation) => { + return { + ...originalImplementation, + getUserInfo: async function (input: Parameters[0]) { + // Call provider's APIs to get profile info + // ... + return { + thirdPartyUserId: "...", + email: { + id: "...", + isVerified: true, }, - // highlight-start - override: (originalImplementation) => { - return { - ...originalImplementation, - getUserInfo: async function (input) { - // Call provider's APIs to get profile info - // ... - return { - thirdPartyUserId: "...", - email: { - id: "...", - isVerified: true - }, - rawUserInfoFromProvider: { - fromUserInfoAPI: { - "first_name": "...", - "last_name": "..." - }, - } - } - } - } - } - // highlight-end - }] - } - }) - ] -}) + rawUserInfoFromProvider: { + fromUserInfoAPI: { + first_name: "...", + last_name: "...", + }, + }, + }; + }, + }; + }, + }, + ], + }, + }), + ], +}); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/thirdparty" @@ -679,7 +658,6 @@ func main() { }, TokenEndpoint: "https://example.com/oauth/token", }, - // highlight-start Override: func(originalImplementation *tpmodels.TypeProvider) *tpmodels.TypeProvider { // ... originalImplementation.GetUserInfo = func(oAuthTokens map[string]interface{}, userContext *map[string]interface{}) (tpmodels.TypeUserInfo, error) { @@ -702,7 +680,6 @@ func main() { } return originalImplementation }, - // highlight-end }, }, }, @@ -711,11 +688,9 @@ func main() { }) } ``` - - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.provider import ProviderClientConfig, ProviderConfig, ProviderInput, Provider @@ -746,11 +721,10 @@ def override_custom_provider(provider: Provider) -> Provider: init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ thirdparty.init( - # highlight-start sign_in_and_up_feature=SignInAndUpFeature( providers=[ ProviderInput( @@ -775,14 +749,12 @@ init( ), ] ) - # highlight-end ) ] ) ``` - - - + + The original implementation has 4 functions which can be overridden: @@ -806,11 +778,11 @@ The original implementation has 4 functions which can be overridden: ## See also - - - - - - - - + + + + + + + + diff --git a/docs/authentication/social/hooks-and-overrides.mdx b/docs/authentication/social/hooks-and-overrides.mdx index 6ff84b9cab..a0ba156c34 100644 --- a/docs/authentication/social/hooks-and-overrides.mdx +++ b/docs/authentication/social/hooks-and-overrides.mdx @@ -1,190 +1,162 @@ --- title: Hooks and overrides -hide_title: true -sidebar_position: 5 -description: >- - Customize the authentication flow to trigger events like analytics or database - updates. -page_type: guide -recipe: thirdparty -category: thirdparty +description: Customize the authentication flow to trigger events like analytics or database updates. +sidebar: + order: 5 --- - -# Hooks and overrides - - -**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. +**SuperTokens** exposes a set of constructs that allow you to trigger different actions during the authentication lifecycle or to even fully customize the logic based on your use case. The following sections describe how you can adjust the `thirdparty` recipe to your needs. -Explore the [references pages](/docs/references) for a more in depth guide on hooks and overrides. +Explore the [references pages](/references) for a more in depth guide on hooks and overrides. ## Frontend hook - + - + This method gets fired, with the `SUCCESS` action, immediately after a successful sign in or sign up. Follow the code snippet to determine if the user is signing up or signing in. With this method you can fire events immediately after a successful sign in. -You can use it to send analytics events. - - - - - +You can use it to send analytics events. + + ```tsx import SuperTokens from "supertokens-auth-react"; import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - ThirdParty.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + Session.init(), + ], }); ``` - - - - - - - -```tsx + + +```tsx check=false reason="This example omits surrounding application and SuperTokens configuration." // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUIThirdParty.init({ - // highlight-start - onHandleEvent: async (context) => { - if (context.action === "SUCCESS") { - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // TODO: Sign up - } else { - // TODO: Sign in - } - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIThirdParty.init({ + onHandleEvent: async (context) => { + if (context.action === "SUCCESS") { + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // TODO: Sign up + } else { + // TODO: Sign in + } + } + }, + }), + supertokensUISession.init(), + ], }); ``` + + + - - - + - - - - -:::caution Not applicable +:::warning[Not applicable] This section is not applicable for custom UI since you are calling the `signInUp` API yourself anyway. You can do anything you want post `signIn` / `signUp` based on the result of the API call. ::: - + ## Backend override Overriding the `signInUp` function allows you to introduce your own logic for the sign in process. Use it to persist different types of data or trigger actions. - - - + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - providers: [/* ... */] - }, - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signInUp: async function (input) { - - // First we call the original implementation of signInUp. - let response = await originalImplementation.signInUp(input); - - // Post sign up response, we check if it was successful - if (response.status === "OK") { - let { id, emails } = response.user; - - // This is the response from the OAuth 2 provider that contains their tokens or user info. - let providerAccessToken = response.oAuthTokens["access_token"]; - let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - - if (input.session === undefined) { - if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { - // TODO: Post sign up logic - } else { - // TODO: Post sign in logic - } - } - } - return response; - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + /* ... */ + ], + }, + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signInUp: async function (input) { + // First we call the original implementation of signInUp. + let response = await originalImplementation.signInUp(input); + + // Post sign up response, we check if it was successful + if (response.status === "OK") { + let { id, emails } = response.user; + + // This is the response from the OAuth 2 provider that contains their tokens or user info. + let providerAccessToken = response.oAuthTokens["access_token"]; + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if (input.session === undefined) { + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // TODO: Post sign up logic + } else { + // TODO: Post sign in logic + } } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + } + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - + + ```go import ( "fmt" @@ -198,7 +170,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ thirdparty.Init(&tpmodels.TypeInput{ - //highlight-start Override: &tpmodels.OverrideStruct{ Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { // create a copy of the originalImplementation @@ -241,16 +212,14 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo from supertokens_python.recipe import thirdparty from supertokens_python.recipe.thirdparty.interfaces import ( @@ -262,7 +231,6 @@ from typing import Dict, Any, Optional, Union from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_thirdparty_functions( original_implementation: RecipeInterface, ) -> RecipeInterface: @@ -326,37 +294,32 @@ def override_thirdparty_functions( return original_implementation -# highlight-end - init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ thirdparty.init( - # highlight-start override=thirdparty.InputOverrideConfig( functions=override_thirdparty_functions ), - # highlight-end sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[]), ) ], ) ``` - - - + + --- ## See also - - - - - - - - + + + + + + + + diff --git a/docs/authentication/social/initial-setup.mdx b/docs/authentication/social/initial-setup.mdx index d8c7a572da..7084977f19 100644 --- a/docs/authentication/social/initial-setup.mdx +++ b/docs/authentication/social/initial-setup.mdx @@ -1,470 +1,709 @@ --- -title: Initial Setup -hide_title: true -sidebar_position: 2 -description: >- - Authenticate using SuperTokens with ThirdParty Providers, creating a custom or - prebuilt login flow. -page_type: tutorial -recipe: thirdparty -category: thirdparty +title: Set Up Social Login +description: Integrate Google, Apple, and other OAuth providers with ThirdParty and Session recipes, callback routes, and prebuilt or custom UI. +sidebar: + label: Initial Setup + order: 2 --- -import ReactRouterCallout from "/docs/_blocks/react-router-callout.mdx"; -import FrontendCustomUIMobileAccessTokenFlow from "./_blocks/frontend-custom-ui-mobile-access-token-flow.mdx" -import FrontendCustomUIMobileAuthorizationCodeFlow from "./_blocks/frontend-custom-ui-mobile-authorization-code-flow.mdx" -import FrontendCustomUIWebAuthorizationCodeFlow from "./_blocks/frontend-custom-ui-web-authorization-code-flow.mdx" -import BackendSDKInit from "./_blocks/backend-sdk-init.mdx" -import BackendProviderInit from "./_blocks/backend-providers-init.mdx" + -# Initial setup +## Social login integration summary -## Overview - -This page shows you how to authenticate, using **ThirdParty Providers**, with **SuperTokens**. -The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. +- Configure the ThirdParty and Session recipes on the frontend and backend. With the prebuilt UI, add the required providers to its frontend provider list; with a custom UI, select the provider when starting authorization. Configure provider credentials on the backend, load secrets from environment variables or a secret manager, and keep them out of source control. +- For Google, use `thirdPartyId: "google"`, provide the Google client ID and secret, and use the same frontend callback URL throughout the flow. A conventional callback is `/auth/callback/google`; call `signInAndUp` when that page loads. +- For Apple, use `thirdPartyId: "apple"` and provide the client ID, key ID, private key, and team ID. Apple sends a form POST to the backend callback instead of redirecting directly to the frontend. +- For Apple, set `redirectURIOnProviderDashboard` to the backend callback and `frontendRedirectURI` to the frontend callback page, which completes authentication by calling `signInAndUp`. -## Before you start + -:::important + +Add SuperTokens social login to this existing application. Inspect the project stack and existing recipes, then ask which providers are required if they cannot be inferred. Configure the frontend and backend ThirdParty and Session recipes, provider client IDs, callback URLs, auth routes, and environment variables. Keep client secrets out of source control, preserve existing conventions, and validate successful login, denied consent, callback failures, and session creation. + -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page please follow the tutorial and return here once you're done. +## Overview -::: +This page shows you how to authenticate, using **ThirdParty Providers**, with **SuperTokens**. +The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**. ## Steps - + - + ### 1. Initialize the frontend SDK - - - + + #### 1.1 Add the `ThirdParty` recipe in your main configuration file. + + + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -// highlight-next-line -import ThirdParty, {Github, Google, Facebook, Apple} from "supertokens-auth-react/recipe/thirdparty"; +import ThirdParty, { Github, Google, Facebook, Apple } from "supertokens-auth-react/recipe/thirdparty"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ -// highlight-start - ThirdParty.init({ - signInAndUpFeature: { - providers: [ - Github.init(), - Google.init(), - Facebook.init(), - Apple.init(), - ] - } - }), - // highlight-end - Session.init() - ] + appInfo: { + // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [Github.init(), Google.init(), Facebook.init(), Apple.init()], + }, + }), + Session.init(), + ], }); ``` + + - - + + #### 1.2 Include the pre-built UI components in your application. In order for the **pre-built UI** to render inside your application, you have to specify which routes show the authentication components. The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. + + - - + + + + ```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; - -// highlight-next-line -import { ThirdPartyPreBuiltUI } from 'supertokens-auth-react/recipe/thirdparty/prebuiltui'; +import React from "react"; +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; + +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the ^{appInfo.websiteBasePath} route*/} - // highlight-next-line - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ThirdPartyPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } } ``` - - - - - + + ```tsx -import React from 'react'; +import React from "react"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -// highlight-next-line -import { ThirdPartyPreBuiltUI } from 'supertokens-auth-react/recipe/thirdparty/prebuiltui'; +import { ThirdPartyPreBuiltUI } from "supertokens-auth-react/recipe/thirdparty/prebuiltui"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { - render() { - // highlight-start - if (canHandleRoute([ThirdPartyPreBuiltUI])) { - // This renders the login UI on the ^{appInfo.websiteBasePath} route - return getRoutingComponent([ThirdPartyPreBuiltUI]) - } - // highlight-end - - return ( - {/*Your app*/} - ); + render() { + if (canHandleRoute([ThirdPartyPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([ThirdPartyPreBuiltUI]); } + return {/*Your app*/}; + } } ``` - - - - - - -Add the `ThirdParty` recipe in your `AuthComponent`. - - ```tsx title="/app/auth/auth.component.ts" - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document - ) { } - - ngAfterViewInit() { - this.loadScript('^{prebuiltUIVersion}'); - } - - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById('supertokens-script'); - if (script) { - script.remove(); - } - } + + + + + + + + + +:::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] +Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. + + + + + + + + + +```tsx +import React from "react"; - private loadScript(src: string) { - const script = this.renderer.createElement('script'); - script.type = 'text/javascript'; - script.src = src; - script.id = 'supertokens-script'; - script.onload = () => { - supertokensUIInit({ - appInfo: { - // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ -// highlight-start - supertokensUIThirdParty.init({ - signInAndUpFeature: { - providers: [ - supertokensUIThirdParty.Github.init(), - supertokensUIThirdParty.Google.init(), - supertokensUIThirdParty.Facebook.init(), - supertokensUIThirdParty.Apple.init(), - ] - } - }), -// highlight-end - supertokensUISession.init(), - ], - }); - } - this.renderer.appendChild(this.document.body, script); - } - } - ``` - - -
- - - -Add the `ThirdParty` recipe in your `AuthView` file. - - ```tsx - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - +import { BrowserRouter, useRoutes } from "react-router-dom"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import * as reactRouterDom from "react-router-dom"; - - ``` +function AppRoutes() { + const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ + ]); + const routes = useRoutes([ + ...authRoutes.map((route) => route.props), + // Include the rest of your app routes + ]); - + return routes; +} -
+function App() { + return ( + + + + + + ); +} +``` + + + + + + + + + +::: + + + + -#### Change the button style +#### Change the button style On the frontend, you can provide a button component to the in-built providers defining your own UI. The component you add is clickable by default. - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; -import ThirdParty, {Google, Github, Facebook, Apple} from "supertokens-auth-react/recipe/thirdparty"; +import ThirdParty, { Google, Github, Facebook, Apple } from "supertokens-auth-react/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - ThirdParty.init({ - signInAndUpFeature: { - // highlight-start - providers: [ - Github.init({ - buttonComponent: (props: {name: string}) =>
- }), - Google.init({ - buttonComponent: (props: {name: string}) =>
- }), - Facebook.init({ - buttonComponent: (props: {name: string}) =>
- }), - Apple.init({ - buttonComponent: (props: {name: string}) =>
- }), - ], - // highlight-end - // ... - }, - // ... - }), + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + Github.init({ + buttonComponent: (props: { name: string }) =>
, + }), + Google.init({ + buttonComponent: (props: { name: string }) =>
, + }), + Facebook.init({ + buttonComponent: (props: { name: string }) =>
, + }), + Apple.init({ + buttonComponent: (props: { name: string }) =>
, + }), + ], // ... - ] + }, + // ... + }), + // ... + ], }); ``` -
- - - -:::caution -This is impossible for non-react apps at the moment. Please use custom UI instead for the sign-in form. -::: - - -
+ + ### 2. Initialize the backend SDK - - -### 3. Add the authentication providers - - - -
+You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. +The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. - + + +```tsx title="Backend SDK Init" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; -### 1. Initialize the frontend SDK - -Call the SDK init function at the start of your application. -The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you are using in your setup. +supertokens.init({ + // Replace this with the framework you are using + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + /*TODO: See next step*/ + }), + Session.init(), + ], +}); +``` + + +```python title="Backend SDK Init" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import thirdparty, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + thirdparty.init( + # TODO: See next step + ) + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + + +```go title="Backend SDK Init" +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{/*TODO: See next step*/}), + session.Init(nil), // initializes session features + }, + }) - - + if err != nil { + panic(err.Error()) + } +} +``` + + - - +### 3. Add the authentication providers +Populate the `providers` array with the third-party authentication providers that you want. + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import ThirdParty from "supertokens-web-js/recipe/thirdparty"; +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - // highlight-next-line - ThirdParty.init(), - Session.init(), - ], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + // Load these credentials from environment variables or a secret manager. + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + { + config: { + thirdPartyId: "github", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + { + config: { + thirdPartyId: "apple", + clients: [ + { + clientId: "", + additionalConfig: { + keyId: "", + privateKey: "", + teamId: "", + }, + }, + ], + }, + }, + ], + }, + }), + // ... + ], }); ``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + // Inside supertokens.Init + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + // Load these credentials from environment variables or a secret manager. + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "github", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "apple", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + AdditionalConfig: map[string]interface{}{ + "keyId": "", + "privateKey": "", + "teamId": "", + }, + }, + }, + }, + }, + }, + }, + }) +} +``` + + +```python +from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig +from supertokens_python.recipe import thirdparty + +# Inside init +thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ + # Load these credentials from environment variables or a secret manager. + ProviderInput( + config=ProviderConfig( + third_party_id="google", + clients=[ + ProviderClientConfig( + client_id="", + client_secret="", + ), + ], + ), + ), + ProviderInput( + config=ProviderConfig( + third_party_id="github", + clients=[ + ProviderClientConfig( + client_id="", + client_secret="", + ) + ], + ), + ), + ProviderInput( + config=ProviderConfig( + third_party_id="apple", + clients=[ + ProviderClientConfig( + client_id="", + additional_config={ + "keyId": "", + "privateKey": "", + "teamId": "" + }, + ), + ], + ), + ), + ]) +) +``` + + - - - - - - -First, you need to add the recipe script tag. +:::note[Replace every credential placeholder with credentials for your own provider application.] +Load secrets from environment variables or a secret manager. Do not commit client secrets or Apple private keys to source control. +Read the list of [built-in providers](/authentication/social/built-in-providers-config) that also includes information on how to generate your own keys. +To add a provider that is not listed, you can follow the guide on [setting up custom providers](/authentication/social/custom-providers). +::: -```html - -``` -You can initialize the SDK. +#### Set OAuth scopes - +To add additional OAuth scopes when accessing your third-party provider, add them to the configuration when initializing the backend `SDK`. +For example, if you are using Google as your third-party provider, you can add an additional scope as follows: + + ```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensThirdParty from 'supertokens-web-js-script/recipe/thirdparty'; -supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - // highlight-next-line - supertokensThirdParty.init(), - ], +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "TODO: GOOGLE_CLIENT_ID", + clientSecret: "TODO: GOOGLE_CLIENT_SECRET", + scope: ["scope1", "scope2"], + }, + ], + }, + }, + ], + }, + }), + ], }); ``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO: GOOGLE_CLIENT_ID", + ClientSecret: "TODO: GOOGLE_CLIENT_SECRET", + Scope: []string{ + "scope1", "scope2", + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="google", + clients=[ + ProviderClientConfig( + client_id="GOOGLE_CLIENT_ID", + client_secret="GOOGLE_CLIENT_SECRET", + scope=["scope1", "scope2"] + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + - +:::note[Along with your custom scopes, also add scopes that ask for the user's email and its verification status. For example, with Google, this scope is `"https://www.googleapis.com/auth/userinfo.email"`.] +::: - + - + - - +### 1. Initialize the frontend SDK - +Call the SDK init function at the start of your application. +The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you are using in your setup. + + + + +Add the `SuperTokens.init` function call at the start of your application. + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import ThirdParty from "supertokens-web-js/recipe/thirdparty"; SuperTokens.init({ - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [ThirdParty.init(), Session.init()], }); ``` + + + + +```tsx +import SuperTokens from "supertokens-react-native"; - - - - - - -Add the `SuperTokens.init` function call at the start of your application. - +SuperTokens.init({ + apiDomain: "", + apiBasePath: "/auth", +}); +``` + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -472,32 +711,26 @@ import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() - - SuperTokens.Builder(this, "^{appInfo.apiDomain}") - .apiBasePath("^{appInfo.apiBasePath}") + + SuperTokens.Builder(this, "") + .apiBasePath("/auth") .build() } } ``` - - - - - - - - + + ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}" + apiDomain: "", + apiBasePath: "/auth" ) } catch SuperTokensError.initError(let message) { // TODO: Handle initialization error @@ -507,45 +740,37 @@ fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { return true } - + } ``` - - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; void main() { SuperTokens.init( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", + apiDomain: "", + apiBasePath: "/auth", ); } ``` + + + + - - - - - - - ### 2. Add the login UI -The **ThirdParty** flow involves creating a UI element that allows the user to initiate the login process. -This occurs through a separate button for each provider that you have configured. You can have a look at the [UI implementation](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/thirdparty-auth--sign-in-up) to get a better idea. +The **ThirdParty** flow involves creating a button for each configured provider so that the user can initiate login. After the user clicks one of those buttons the actions that you need to take differ based on which type of authentication scenario you are using: -- **Authorization Code** +- **Authorization Code** This option can either involve a **Client Secret** configured on the backend or rely on **Proof Key for Code Exchange (PKCE)** exchange. The difference between the two is that the first option uses a private secret, on the backend, to get the access token. @@ -555,91 +780,1384 @@ Regardless of which authentication type you are using, in the end, the access to - **OAuth/Access Tokens** This option only applies to mobile/desktop apps. -The frontend obtains the Access Token and then sends it to the backend. +The frontend obtains the access token and then sends it to the backend. SuperTokens then fetches user info using the access token and logs them in. - - - - - - - - - - - - - - - :::info - - This flow is not applicable for web apps. - - ::: - - - - - - - - +#### Authorization Code + + + +##### Redirecting to a social/single sign-on provider + +The first step is to fetch the URL on which the user authenticates. You can do this by querying the backend API exposed by SuperTokens (as shown below). The backend SDK automatically appends the right query params to the URL (like scope, client ID etc). + +After getting the URL, redirect the user there. In the code below, an example of login with Google appears: + + +##### Sign in with Apple example + + + +###### Fetching the authorization code on the frontend + +For React Native apps, set up the [react-native-apple-authentication library](https://github.com/invertase/react-native-apple-authentication). Follow its `README`, and request the email scope when your application uses email identity. Apple may return the user's actual address or a private relay address, and the native credential may include it only on the first authorization. + +Once the integration is complete, call `appleAuth.performRequest` on iOS or `appleAuthAndroid.signIn` on Android. Send the one-time authorization code to your backend as shown in the next step. + +A full example of this is available in [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/apple.ts). + +If you use Expo, you can use the [expo-apple-authentication](https://docs.expo.dev/versions/latest/sdk/apple-authentication/) library instead (note that this library only works on iOS). + + +###### Fetching the authorization code on the frontend + +:::info[At the moment this flow is not supported on Android.] +::: + + +###### Fetching the authorization code on the frontend + +For iOS, use the native Sign in with Apple flow, then send the authorization code to SuperTokens. You can see a full example of this in the `onAppleClicked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). + + +###### Fetching the authorization code on the frontend + +For Flutter, use the [`sign_in_with_apple`](https://pub.dev/packages/sign_in_with_apple) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the apple sign-in flow. You can see a full example of this in the `loginWithApple` function in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). + + + + + + + +```tsx +import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; + +async function googleSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "google", + + // This is where Google should redirect the user back after login or error. + // Configure this URL on the Google provider dashboard as well. + frontendRedirectURI: "https:///auth/callback/google", + }); + + /* + Example value of authUrl: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&access_type=offline&include_granted_scopes=true&response_type=code&client_id=&state=5a489996a28cafc83ddff&redirect_uri=https%3A%2F%2Fsupertokens.io%2Fdev%2Foauth%2Fredirect-to-app&flowName=GeneralOAuthFlow + */ + + // Redirect the user to Google for authentication. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + + + +```swift +import UIKit +import AuthenticationServices + +fileprivate class ViewController: UIViewController, ASAuthorizationControllerPresentationContextProviding, ASAuthorizationControllerDelegate { + func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return view.window! + } + + func loginWithApple() { + let authorizationRequest = ASAuthorizationAppleIDProvider().createRequest() + authorizationRequest.requestedScopes = [.email, .fullName] + + let authorizationController = ASAuthorizationController(authorizationRequests: [authorizationRequest]) + + authorizationController.presentationContextProvider = self + authorizationController.delegate = self + authorizationController.performRequests() + } + + func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + guard let credential: ASAuthorizationAppleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential, + let authorizationCode = credential.authorizationCode, + let authorizationCodeString = String(data: authorizationCode, encoding: .utf8) else { return } + + let email = credential.email + let firstName = credential.fullName?.givenName + let lastName = credential.fullName?.familyName + + // Send the required authorization code and any profile values Apple returned to the backend. + // Persist first-login profile values if your application needs them; Apple may omit them later. + } +} +``` + + +```dart +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:sign_in_with_apple/sign_in_with_apple.dart'; + +Future createAppleMobileTransaction() async { + final response = await http.post( + Uri.parse("/apple-mobile-transactions"), + headers: { + "Authorization": "Bearer ", + "Content-Type": "application/json", + "X-App-Installation-ID": "", + }, + body: jsonEncode({ + "appType": "android", + "clientType": "", + }), + ); + if (response.statusCode != 201) { + throw StateError("Could not create Apple login transaction"); + } + + return jsonDecode(response.body)["transactionId"] as String; +} + +void loginWithApple() async { + try { + String? transactionId; + if (Platform.isAndroid) { + transactionId = await createAppleMobileTransaction(); + // Keep a copy in memory until the callback returns. + } + + var credential = await SignInWithApple.getAppleIDCredential( + scopes: [ + AppleIDAuthorizationScopes.email, + AppleIDAuthorizationScopes.fullName, + ], + state: transactionId, + // Required for Android only + webAuthenticationOptions: WebAuthenticationOptions( + clientId: "", + redirectUri: Uri.parse( + "//callback/apple", + ), + ), + ); + + String authorizationCode = credential.authorizationCode; + String? idToken = credential.identityToken; + String? email = credential.email; + String? firstname = credential.givenName; + String? lastName = credential.familyName; + + if (transactionId != null && credential.state != transactionId) { + throw StateError("Apple login transaction mismatch"); + } + + // Send the user information and auth code to the backend. Refer to the next step. + } catch (e) { + // Sign in aborted or failed + } +} +``` + + + + + + + + + + + +Apple may return the user's email and full name only the first time the user authorizes your app. Treat those fields as +optional, but require the one-time authorization code. If your application needs the profile values, store them during +the first successful login rather than requiring Apple to return them again. + + + +##### Handling the auth callback on your frontend + +Once the third-party provider redirects your user back to your app, you need to consume the information to sign in the user. This requires you to: + +- Set up a route in your app that handles this callback. It's recommended to use something like `https:///auth/callback/google` (for Google). Regardless of what you make this path, remember to use that same path when calling the `getAuthorisationURLWithQueryParamsAndSetState` function in the first step. + +- On that route, call the following function on page load + + +###### Additional steps for Android + +For Android, a way for the web login flow to redirect back to the app is also needed. By default, the API provided by the backend `SDKs` redirects to the website domain you provide when initializing the `SDK`. The API can be overridden to redirect to the app instead. For example, if using the Node.js `SDK`: + +Before starting authorization, have the mobile app request an Apple login transaction from an application endpoint on +your backend. This is not a SuperTokens API. Generate at least 256 random bits, prefix the identifier with +`mobile.`, and store only the opaque identifier server-side with: + +- the app and configured SuperTokens `clientType`; +- the exact Apple callback URL and an allowlisted app deep-link target; +- a hash of the initiating app installation, authenticated session, or browser context when one is available; and +- an expiry no more than five minutes in the future. + +Return the identifier to the initiating app over HTTPS and pass it through the provider's `state` field. The +`sign_in_with_apple` API exposes `state` but no separate transaction field, so `state` transports this app-defined, +namespaced transaction identifier. Do not treat SuperTokens' web state as this mobile transaction. The app must also +compare the returned identifier with the value it stored locally before sending the authorization code to `/signinup`. +Generate the random portion with `crypto.randomBytes(32)` in Node.js, `crypto/rand.Read` in Go, or +`secrets.token_urlsafe(32)` in Python. Never accept a callback or deep-link URI directly from the mobile request; select +both from a server-side allowlist for the requested app and client type. + +At the Apple callback, reject a missing state. Values in the `mobile.` namespace must be consumed with one atomic +database operation that verifies every binding and expiry. Reject invalid, mismatched, expired, or previously consumed +transactions; never fall back to the web handler for one of these failures. Non-mobile state values can be passed to the +original SuperTokens web handler. + +For example, the application transaction store can atomically consume a PostgreSQL row with: + +```sql +DELETE FROM apple_login_transactions +WHERE id = $1 + AND app_type = $2 + AND client_type = $3 + AND expected_callback = $4 + AND expires_at > CURRENT_TIMESTAMP +RETURNING app_redirect_uri, initiating_context_hash; +``` + +Create `id` as a primary key and never reinsert an identifier. The delete and validation must be one database statement, +not a read followed by a delete. The `consumeAppleMobileTransaction` functions referenced below are application code +that execute this query and return the stored, allowlisted redirect URI; they are not SuperTokens SDK APIs. +Apple's provider POST does not contain the originating app's local context. Bind that context when creating the row, +redirect only to the stored target, and require the app to compare the returned transaction identifier with its locally +stored value before continuing. For browser flows, keep using the SuperTokens web state and original callback handler. + +**Node.js** + + + +In the snippet above for Android, you need an additional `webAuthenticationOptions` property when signing in with Apple. +This is because on Android the library uses the web login flow and requires the client ID and redirection URI. +The `redirectUri` property here is the URL to which Apple makes a `POST` request after the user has logged in. +The SuperTokens backend SDKs provide an API for this at `/`<API_BASE_PATH>`/callback/apple`. +Set the app-defined transaction identifier as the `state` argument to `SignInWithApple.getAppleIDCredential` before +starting authorization. + + + + + + + +```tsx +import { signInAndUp } from "supertokens-web-js/recipe/thirdparty"; + +async function handleGoogleCallback() { + try { + const response = await signInAndUp(); + + if (response.status === "OK") { + console.log(response.user); + if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) { + // sign up successful + } else { + // sign in successful + } + window.location.assign("/home"); + } else if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in / up was not allowed. + window.alert(response.reason); + } else { + // The provider did not supply the email identity required by this configuration. + window.alert("No email provided by social login. Please use another form of login"); + window.location.assign("/auth"); // redirect back to login page + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +```tsx +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +ThirdParty.init({ + override: { + apis: (original) => { + return { + ...original, + appleRedirectHandlerPOST: async (input) => { + if (original.appleRedirectHandlerPOST === undefined) { + throw Error("Should never come here"); + } + + const transactionId = input.formPostInfoFromProvider.state; + if (typeof transactionId !== "string" || transactionId.length === 0) { + input.options.res.setStatusCode(400); + input.options.res.sendHTMLResponse("Invalid Apple login transaction"); + return; + } + + if (!transactionId.startsWith("mobile.")) { + return await original.appleRedirectHandlerPOST(input); + } + + const transaction = await consumeAppleMobileTransaction({ + id: transactionId, + appType: "android", + clientType: "", + expectedCallback: "/auth/callback/apple", + }); + + if (transaction === undefined) { + input.options.res.setStatusCode(400); + input.options.res.sendHTMLResponse("Invalid Apple login transaction"); + return; + } + + const query = new URLSearchParams(); + for (const [key, value] of Object.entries(input.formPostInfoFromProvider)) { + query.set(key, `${value}`); + } + + const redirectUrl = `${transaction.appRedirectURI}?${query.toString()}#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end`; + input.options.res.setHeader("Location", redirectUrl, false); + input.options.res.setStatusCode(303); + input.options.res.sendHTMLResponse(""); + }, + }; + }, + }, +}); +``` + + + + + +:::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend SDK.] +::: + +##### Special case for login with Apple + +Unlike other providers, Apple does not redirect your user back to your frontend app. Instead, it redirects the user to your backend with a `FORM POST` request. This means that the URL you configure on Apple's dashboard should point to your backend API layer. Here, **middleware** handles the request and redirects the user to your frontend app. Your frontend app should then call the `signInAndUp` API on that page as shown previously. + +To tell SuperTokens which frontend route to redirect the user back to, set the `frontendRedirectURI` to the frontend route. Also, set the `redirectURIOnProviderDashboard` to point to your backend API route, to which Apple sends a `POST` request. + +Follow Apple's official [Configure Sign in with Apple for the web](https://developer.apple.com/help/account/capabilities/configure-sign-in-with-apple-for-the-web/) guide when creating the Services ID and registering the return URL. + + +**Go** + + + + + +```tsx +import { getAuthorisationURLWithQueryParamsAndSetState } from "supertokens-web-js/recipe/thirdparty"; + +async function appleSignInClicked() { + try { + const authUrl = await getAuthorisationURLWithQueryParamsAndSetState({ + thirdPartyId: "apple", + + frontendRedirectURI: "https:///auth/callback/apple", // This is an example callback URL on your frontend. You can use another path as well. + redirectURIOnProviderDashboard: "/auth/callback/apple", // Configure this URL on the Apple developer dashboard. + }); + + // Redirect the user to Apple for authentication. + window.location.assign(authUrl); + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + + +Pass your transaction-consumption implementation to `initThirdPartyWithAppleMobile`, and include the returned recipe in your SuperTokens `RecipeList`. The helper must return an error for missing, expired, or mismatched transactions. Add your Apple provider configuration to the `TypeInput` below. + +```go +import ( + "net/http" + "net/url" + "strings" + + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +type AppleMobileTransaction struct { + AppRedirectURI string +} + +func initThirdPartyWithAppleMobile(consumeAppleMobileTransaction func(transactionID, appType, clientType, expectedCallback string) (AppleMobileTransaction, error)) supertokens.Recipe { + return thirdparty.Init(&tpmodels.TypeInput{ + Override: &tpmodels.OverrideStruct{ + APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { + originalAppleRedirectPost := *originalImplementation.AppleRedirectHandlerPOST + + *originalImplementation.AppleRedirectHandlerPOST = func(formPostInfoFromProvider map[string]interface{}, options tpmodels.APIOptions, userContext *map[string]interface{}) error { + transactionID, ok := formPostInfoFromProvider["state"].(string) + if !ok || transactionID == "" { + http.Error(options.Res, "Invalid Apple login transaction", http.StatusBadRequest) + return nil + } + + if !strings.HasPrefix(transactionID, "mobile.") { + return originalAppleRedirectPost(formPostInfoFromProvider, options, userContext) + } + + transaction, err := consumeAppleMobileTransaction( + transactionID, + "android", + "", + "/auth/callback/apple", + ) + if err != nil { + http.Error(options.Res, "Invalid Apple login transaction", http.StatusBadRequest) + return nil + } + + queryParams := url.Values{} + for key, value := range formPostInfoFromProvider { + if stringValue, ok := value.(string); ok { + queryParams.Set(key, stringValue) + } + } + + redirectURI := transaction.AppRedirectURI + "?" + queryParams.Encode() + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" + options.Res.Header().Set("Location", redirectURI) + options.Res.WriteHeader(http.StatusSeeOther) + return nil + } + + return originalImplementation + }, + }, + }) +} +``` + + + + + +:::info[If you are using the **Authorization Code Grant** flow with **PKCE** you do **not** need to provide a client secret during backend init.] +This only works for providers which support the [PKCE flow](https://oauth.net/2/pkce/). + +::: + + +**Python** + + + + + +```python +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty.interfaces import APIInterface, APIOptions +from typing import Dict, Any +from urllib.parse import urlencode + +def override_thirdparty_apis(original_implementation: APIInterface): + original_apple_redirect_post = original_implementation.apple_redirect_handler_post + + async def apple_redirect_handler_post( + form_post_info: Dict[str, Any], + api_options: APIOptions, + user_context: Dict[str, Any] + ): + transaction_id = form_post_info.get("state") + if not isinstance(transaction_id, str) or not transaction_id: + api_options.response.set_status_code(400) + api_options.response.set_html_content("Invalid Apple login transaction") + return + + if not transaction_id.startswith("mobile."): + return await original_apple_redirect_post(form_post_info, api_options, user_context) + + transaction = await consume_apple_mobile_transaction( + id=transaction_id, + app_type="android", + client_type="", + expected_callback="/auth/callback/apple", + ) + if transaction is None: + api_options.response.set_status_code(400) + api_options.response.set_html_content("Invalid Apple login transaction") + return + + redirect_url = transaction.app_redirect_uri + "?" + urlencode(form_post_info) + "#Intent;package=YOUR.PACKAGE.IDENTIFIER;scheme=signinwithapple;end" + api_options.response.set_header("Location", redirect_url) + api_options.response.set_status_code(303) + api_options.response.set_html_content("") + + original_implementation.apple_redirect_handler_post = apple_redirect_handler_post + return original_implementation + +thirdparty.init( + override=thirdparty.InputOverrideConfig( + apis=override_thirdparty_apis + ), +) +``` + + + + + +In the code above, the `appleRedirectHandlerPOST` API rejects missing state. The explicit `mobile.` namespace selects the mobile flow; absence of state never does. A namespaced value must match and atomically consume a bound transaction before the handler redirects to the allowlisted deep link. Any transaction failure returns `400` instead of falling back to the web flow. Other non-empty values remain SuperTokens web state and go to the original handler. Follow the `sign_in_with_apple` README to configure the Android deep link. + +###### Calling the `signinup` API to consume the authorization code + +Once you have the authorization code from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: + + + + + +```bash +curl --location --request POST '/auth/signinup' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "thirdPartyId": "apple", + "clientType": "...", + "redirectURIInfo": { + "redirectURIOnProviderDashboard": "/auth/callback/apple", + "redirectURIQueryParams": { + "code": "...", + "user": { + "name":{ + "firstName":"...", + "lastName":"..." + }, + "email":"..." + } + } + } +}' +``` + + + + + +:::note[- On iOS, the client ID set in the backend should be the same as the bundle identifier for your app.] + +- The `clientType` input is optional and required only if you initialize more than one client in the provider on the backend (See the "Social / `SSO` login for both, web and mobile apps" section below). +- On iOS, `redirectURIOnProviderDashboard` doesn't matter and its value can be a universal link configured for your app. +- On Android, the `redirectURIOnProviderDashboard` should match the one configured on the Apple developer dashboard. +- The `user` object contains optional first-login information provided by Apple. Omit `user`, `name`, or `email` when Apple does not return those values; always send the authorization code. +::: + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. +- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +:::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] +::: + +##### Sign in with Google example + + + +###### Fetching the authorization code on the frontend + +This involves setting up the [@react-native-google-signin/google-signin](https://github.com/react-native-google-signin/google-signin) in your app. See their `README` for steps on how to integrate their `SDK` into your application. The minimum scope required by SuperTokens is the one that gives the user's email. + +Once you configure the library, use `GoogleSignin.configure` and `GoogleSignin.signIn` to trigger the login flow and sign the user in with Google. Refer to [the example app](https://github.com/supertokens/supertokens-react-native/blob/master/examples/with-thirdparty/google.ts) to see the full code for this. + + +###### Fetching the authorization code on the frontend + +Follow the [official Google Sign In guide](https://developers.google.com/identity/sign-in/android/start-integrating) to set up their library and sign the user in with Google. Fetch the authorization code from the Google sign-in result. For a full example, refer to the `signInWithGoogle` function in [the example app](https://github.com/supertokens/supertokens-android/blob/master/examples/with-thirdparty/app/src/main/java/com/supertokens/supertokensexample/LoginActivity.kt). + + +###### Fetching the authorization code on the frontend + +For iOS, use the `GoogleSignIn` library. Follow the [official guide](https://developers.google.com/identity/sign-in/ios/start-integrating) to set up the library and sign the user in with Google. Use the result of Google sign-in to get the authorization code. For a full example, refer to the `onGoogleCliked` function in [the example app](https://github.com/supertokens/supertokens-ios/blob/master/examples/with-thirdparty/with-thirdparty/LoginScreen/LoginScreenViewController.swift). + + +###### Fetching the authorization code on the frontend + +For Flutter, use the [`google_sign_in`](https://pub.dev/packages/google_sign_in) package. Make sure to follow the prerequisite steps to get the package setup. After setup, use the snippet below to trigger the Google sign-in flow. For a full example, refer to the `loginWithGoogle` in [the example app](https://github.com/supertokens/supertokens-flutter/blob/master/examples/with-thirdparty/lib/screens/login.dart). + + + + + + + + + +```tsx +import { GoogleSignin } from "@react-native-google-signin/google-signin"; + +export const performGoogleSignIn = async (): Promise => { + GoogleSignin.configure({ + webClientId: "GOOGLE_WEB_CLIENT_ID", + iosClientId: "GOOGLE_IOS_CLIENT_ID", + }); + + try { + const response = await GoogleSignin.signIn({}); + const authCode = response.data?.serverAuthCode; + + // Refer to step 2 + + return true; + } catch (e) { + console.log("Google sign in failed with error", e); + } + + return false; +}; +``` + + +```kotlin +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.util.Log +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import android.content.Intent + +class LoginActivity : AppCompatActivity() { + private lateinit var googleResultLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + googleResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + onGoogleResultReceived(it) + } + } + + private fun signInWithGoogle() { + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestServerAuthCode("GOOGLE_WEB_CLIENT_ID") + .requestEmail() + .build() + + val googleClient = GoogleSignIn.getClient(this, gso) + val signInIntent = googleClient.signInIntent + + googleResultLauncher.launch(signInIntent) + } + + private fun onGoogleResultReceived(it: ActivityResult) { + val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) + val account = task.result + val authCode = account.serverAuthCode + + // Refer to step 2 + } +} +``` + + +```swift +import UIKit +import GoogleSignIn + +fileprivate class LoginScreenViewController: UIViewController { + @IBAction func onGoogleCliked() { + GIDSignIn.sharedInstance.signIn(withPresenting: self) { signInResult, error in + guard error == nil else { return } + + guard let authCode: String = signInResult?.serverAuthCode as? String else { + print("Google login did not return an authorization code") + return + } + + // Refer to step 2 + } + } +} +``` + + +```dart +import 'package:google_sign_in/google_sign_in.dart'; +import 'dart:io'; + +Future loginWithGoogle() async { + GoogleSignIn googleSignIn; + + if (Platform.isAndroid) { + googleSignIn = GoogleSignIn( + serverClientId: "GOOGLE_WEB_CLIENT_ID", + scopes: [ + 'email', + ], + ); + } else { + googleSignIn = GoogleSignIn( + clientId: "GOOGLE_IOS_CLIENT_ID", + serverClientId: "GOOGLE_WEB_CLIENT_ID", + scopes: [ + 'email', + ], + ); + } + + GoogleSignInAccount? account = await googleSignIn.signIn(); + + if (account == null) { + print("Google sign in was aborted"); + return; + } + + String? authCode = account.serverAuthCode; + + if (authCode == null) { + print("Google sign in did not return a server auth code"); + return; + } + + // Refer to step 2 + } +``` + + + + + + + +###### Step 2) Calling the `signinup` API to consume the authorization code + +Once you have the authorization code from the auth provider, you need to call the `signinup` API exposed by the backend `SDK` as shown below: + + + + + +```bash +curl --location --request POST '/auth/signinup' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "thirdPartyId": "google", + "clientType": "...", + "redirectURIInfo": { + "redirectURIOnProviderDashboard": "", + "redirectURIQueryParams": { + "code": "...", + } + } +}' +``` + + + + + +:::note[When calling the API exposed by the SuperTokens backend `SDK`, pass an empty string for `redirectURIOnProviderDashboard`.] +The native login flow using the authorization code does not involve any redirection on the frontend. +::: + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. +- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +:::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] +::: + +##### Authorization code grant flow with `PKCE` + +This is similar to the first one, except that you do **not** need to provide a client secret during backend init. +This flow only works for providers which support the [`PKCE` flow](https://oauth.net/2/pkce/). + +###### Calling the `signinup` API to consume the authorization code + +Once you have the authorization code and `PKCE` verifier from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: + + + +###### Fetching the authorization code on the frontend + +You can use the [react native auth library](https://github.com/FormidableLabs/react-native-app-auth) to also return the `PKCE` code verifier along with the authorization code. Achieve this by setting the `usePKCE` boolean to `true` and also by setting the `skipCodeExchange` to `true` when configuring the react native auth library. + + +###### Fetching the authorization code on the frontend + +You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library to use the `PKCE` flow by using the `setCodeVerifier` method when creating a `AuthorizationRequest`. + + +###### Fetching the authorization code on the frontend + +You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library to use the `PKCE` flow. + + +###### Fetching the authorization code on the frontend + +You can use [`flutter_appauth`](https://pub.dev/packages/flutter_appauth) to use the `PKCE` flow by providing a `codeVerifier` when you call the `appAuth.token` function. + + + + + + + +```bash +curl --location --request POST '/auth/signinup' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "thirdPartyId": "THIRD_PARTY_ID", + "clientType": "...", + "redirectURIInfo": { + "redirectURIOnProviderDashboard": "REDIRECT_URI", + "redirectURIQueryParams": { + "code": "...", + }, + "pkceCodeVerifier": "..." + } +}' +``` + + + + + +:::note[- Replace `THIRD_PARTY_ID` with the provider id. The provider id must match the one you configure in the backend when initializing SuperTokens.] + +- `REDIRECT_URI` must exactly match the value you configure on the providers dashboard. +::: + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. +- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +:::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] +::: + + + +#### OAuth/Access Tokens + + + +:::info[This flow is not applicable for web apps.] + +::: + + +##### Fetching the OAuth/Access tokens on the frontend + +1. Sign in with the social provider. The minimum required scope is the one that provides access to the user's email. You can use any library to sign in with the social provider. +2. Get the access token on the frontend if it is available. +3. Get the id token from the sign in result if it is available. + +:::note[You need to provide either the access token or the id token, or both in step 2, depending on what is available.] +::: + +##### Calling the `signinup` API to use the OAuth tokens + +Once you have the `access_token` or the `id_token` from the auth provider, you need to call the `/signinup` API exposed by the backend `SDK` as shown below: + + + + + + + + +```bash +curl --location --request POST '/auth/signinup' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "thirdPartyId": "google", + "clientType": "...", + "oAuthTokens": { + "access_token": "...", + "id_token": "..." + }, +}' +``` + + + + + +:::note[- The `clientType` input is optional, and you need it only if you have initialised more than one client in the provider on the backend (See the "Social / Single Sign-On login for both, web and mobile apps" section below).] + +- If you have the `id_token`, you can send that along with the `access_token`. +::: + +The response body from the API call has a `status` property in it: + +- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user. +- `status: "NO_EMAIL_GIVEN_BY_PROVIDER"`: The provider did not return the email identity required by this configuration. Ask the user to choose another sign-in method. Do not invent an email address without first defining a stable, provider-specific identity and an [account-linking policy](/post-authentication/account-linking/important-concepts): synthetic addresses can create duplicate accounts, link the wrong identities, and invalidate assumptions that an email belongs to or was verified by the user. +- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should display on the frontend. +- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed. + +:::note[On success, the backend sends back session tokens as part of the response headers which are automatically handled by the frontend `SDK` for you.] +::: + + ### 3. Initialize the backend SDK - +You have to initialize the **Backend Software Development Kit (SDK)** alongside the code that starts your server. +The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup. + + + +```tsx title="Backend SDK Init" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +supertokens.init({ + // Replace this with the framework you are using + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + ThirdParty.init({ + /*TODO: See next step*/ + }), + Session.init(), + ], +}); +``` + + +```python title="Backend SDK Init" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import thirdparty, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + thirdparty.init( + # TODO: See next step + ) + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + + +```go title="Backend SDK Init" +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{/*TODO: See next step*/}), + session.Init(nil), // initializes session features + }, + }) + + if err != nil { + panic(err.Error()) + } +} +``` + + ### 4. Add the authentication providers - +Populate the `providers` array with the third-party authentication providers that you want. + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + // Load these credentials from environment variables or a secret manager. + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + { + config: { + thirdPartyId: "github", + clients: [ + { + clientId: "", + clientSecret: "", + }, + ], + }, + }, + { + config: { + thirdPartyId: "apple", + clients: [ + { + clientId: "", + additionalConfig: { + keyId: "", + privateKey: "", + teamId: "", + }, + }, + ], + }, + }, + ], + }, + }), + // ... + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" +) + +func main() { + // Inside supertokens.Init + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + // Load these credentials from environment variables or a secret manager. + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "github", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + ClientSecret: "", + }, + }, + }, + }, + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "apple", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "", + AdditionalConfig: map[string]interface{}{ + "keyId": "", + "privateKey": "", + "teamId": "", + }, + }, + }, + }, + }, + }, + }, + }) +} +``` + + +```python +from supertokens_python.recipe.thirdparty.provider import ProviderInput, ProviderConfig, ProviderClientConfig +from supertokens_python.recipe import thirdparty + +# Inside init +thirdparty.init( + sign_in_and_up_feature=thirdparty.SignInAndUpFeature(providers=[ + # Load these credentials from environment variables or a secret manager. + ProviderInput( + config=ProviderConfig( + third_party_id="google", + clients=[ + ProviderClientConfig( + client_id="", + client_secret="", + ), + ], + ), + ), + ProviderInput( + config=ProviderConfig( + third_party_id="github", + clients=[ + ProviderClientConfig( + client_id="", + client_secret="", + ) + ], + ), + ), + ProviderInput( + config=ProviderConfig( + third_party_id="apple", + clients=[ + ProviderClientConfig( + client_id="", + additional_config={ + "keyId": "", + "privateKey": "", + "teamId": "" + }, + ), + ], + ), + ), + ]) +) +``` + + + + +:::note[Replace every credential placeholder with credentials for your own provider application.] +Load secrets from environment variables or a secret manager. Do not commit client secrets or Apple private keys to source control. +Read the list of [built-in providers](/authentication/social/built-in-providers-config) that also includes information on how to generate your own keys. +To add a provider that is not listed, you can follow the guide on [setting up custom providers](/authentication/social/custom-providers). +::: + + +#### Set OAuth scopes + +To add additional OAuth scopes when accessing your third-party provider, add them to the configuration when initializing the backend `SDK`. + +For example, if you are using Google as your third-party provider, you can add an additional scope as follows: + + + +```tsx +import SuperTokens from "supertokens-node"; +import ThirdParty from "supertokens-node/recipe/thirdparty"; + +SuperTokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "google", + clients: [ + { + clientId: "TODO: GOOGLE_CLIENT_ID", + clientSecret: "TODO: GOOGLE_CLIENT_SECRET", + scope: ["scope1", "scope2"], + }, + ], + }, + }, + ], + }, + }), + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/thirdparty" + "github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + thirdparty.Init(&tpmodels.TypeInput{ + SignInAndUpFeature: tpmodels.TypeInputSignInAndUp{ + Providers: []tpmodels.ProviderInput{ + { + Config: tpmodels.ProviderConfig{ + ThirdPartyId: "google", + Clients: []tpmodels.ProviderClientConfig{ + { + ClientID: "TODO: GOOGLE_CLIENT_ID", + ClientSecret: "TODO: GOOGLE_CLIENT_SECRET", + Scope: []string{ + "scope1", "scope2", + }, + }, + }, + }, + }, + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.thirdparty import ProviderInput, ProviderConfig, ProviderClientConfig, SignInAndUpFeature + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + thirdparty.init( + + sign_in_and_up_feature=SignInAndUpFeature( + providers=[ + ProviderInput( + config=ProviderConfig( + third_party_id="google", + clients=[ + ProviderClientConfig( + client_id="GOOGLE_CLIENT_ID", + client_secret="GOOGLE_CLIENT_SECRET", + scope=["scope1", "scope2"] + ), + ], + ), + ), + ] + ) + ) + ] +) +``` + + + +:::note[Along with your custom scopes, also add scopes that ask for the user's email and its verification status. For example, with Google, this scope is `"https://www.googleapis.com/auth/userinfo.email"`.] +::: - + ## Next steps Having completed the main setup, you can explore more advanced topics related to the **ThirdParty** recipe. - - - - Built-in Providers - - - Read more about the common providers exposed by the recipe. - - - - - Custom Providers - - - See how you can create your own custom provider. - - - - - Custom Invite Flow - - - Disable public sign ups and use your own invite flow. - - - - - Hooks and Overrides - - - Add custom logic after the logs in or signs up. - - - + + +Read more about the common providers exposed by the recipe. + + +See how you can create your own custom provider. + + +Disable public sign ups and use your own invite flow. + + +Add custom logic after the logs in or signs up. + + diff --git a/docs/authentication/social/introduction.mdx b/docs/authentication/social/introduction.mdx index 4712db856b..1d28777980 100644 --- a/docs/authentication/social/introduction.mdx +++ b/docs/authentication/social/introduction.mdx @@ -1,87 +1,60 @@ --- -title: Introduction -hide_title: true -sidebar_position: 1 -pagination_next: null -pagination_prev: null -skip_llms_txt: true -description: >- - Authenticate users with third-party providers using pre-built UI or custom SDK - interfaces. -page_type: overview -recipe: thirdparty -category: thirdparty +title: Social Login +description: Authenticate users with built-in or custom third-party identity providers using prebuilt UI or custom SDK interfaces. +sidebar: + label: Overview + order: 1 --- + -# Introduction +## Social login summary + +- The ThirdParty recipe authenticates users through third-party providers with either the prebuilt UI or a custom SDK-based interface. +- Configure a built-in provider such as Google or Apple, or implement a custom provider. +- Hooks and overrides add custom sign-in logic. A custom invite flow can disable public sign-ups. + + ## Overview The **ThirdParty** `recipe` provides a way of authenticating users through a third party provider. You can use it out of the box, with the **Pre-Built UI**, or implement your own interface through the available SDKs. -Sign in form UI for passwordless login + + Sign in form UI for social login + -## Getting started +## Getting started You can either follow the quickstart tutorial or use the `CLI` tool to generate an example app that shows you how the recipe works. - - - - Quickstart - - - Go through a quick tutorial that shows you how to add the **ThirdParty** recipe to your app. - - - - - Example Applications - - - Use the CLI to generate a boilerplate app that you can use as a starting point. - - - + + +Go through a quick tutorial that shows you how to add the **ThirdParty** recipe to your app. + + -## Customization +## Customization To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - Built-in Providers - - - Read more about the common providers exposed by the recipe. - - - - - Custom Providers - - - See how you can create your own custom provider. - - - - - Hooks and Overrides - - - Add custom logic after the logs in or signs up. - - - - - Custom Invite Flow - - - Disable public sign ups and use your own invite flow. - - - + + +Read more about the common providers exposed by the recipe. + + +See how you can create your own custom provider. + + +Add custom logic after the logs in or signs up. + + +Disable public sign ups and use your own invite flow. + + diff --git a/docs/authentication/social/meta.ts b/docs/authentication/social/meta.ts new file mode 100644 index 0000000000..78879f4078 --- /dev/null +++ b/docs/authentication/social/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Social Login", + icon: "circle-user-round", + order: 40, +}); diff --git a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-details.mdx b/docs/authentication/unified-login/_blocks/create-oauth2-client-request-details.mdx deleted file mode 100644 index 80462f85e1..0000000000 --- a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-details.mdx +++ /dev/null @@ -1,102 +0,0 @@ - - - - Creates an OAuth2 client - - - - -**Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. - - - - - - -

Request

- -

Body Schema

- -| Name | Type | Description | Required | Default Value | -|--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| -| `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | -| `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | -| `redirectUris` | `array` of `string` | List of URIs where the Authorization Server redirects after authorization. Can include mobile/desktop deep links. Must be exact URLs without wildcards. | Yes | - | | `scope` | `string` | Space-separated string of scopes the Client will request. Include `offline_access` to enable OAuth2 Refresh Token usage. | Yes | - | -| `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | -| `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | -| `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | -| `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | -| `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | -| `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | -| `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | -| `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | -| `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | - - -

GrantType

- -- `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. -- `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. -- `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. - -

TokenEndpointAuthMethod

- -- `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. -- `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. -- `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. -- `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). - -

ResponseType

- -- `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. -- `id_token`: Indicates that the Client expects an ID Token. - -

Time Duration

- -A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. - -

Example

- -```bash -curl -X POST ^{coreInfo.uri}/recipe/oauth/clients \ - -H "Content-Type: application/json" \ - -H "api-key: ^{coreInfo.key}" \ - -d '{ - "clientName": "", - "responseTypes": ["code"], - "grantTypes": ["authorization_code", "refresh_token"], - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - }' -``` - -

Response

- -

200

- -The client has been successfully created. - -

Response schema

- -| Property | Type | Description | -|-------------|----------------------------------|-----------------------------------------------| -| `clientName` | `string` | The name of the client. | -| `clientId` | `string` | Unique identifier for the client. | -| `clientSecret` | `string` | The secret is part of the create request as plain text, and then never again. The secret remains in hashed format and is not recoverable once lost. | -| `redirectUris` | `array` of `string` | The URLs used for redirection. | -| `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | -| `scope` | `array` of `string` | A space separated string of scopes that the Client requests access to. | - -

Example

- -```json -{ - "clientName": "", - "clientId": "", - "redirectUris": ["https:///oauth/callback"], - "audience": [""], - "scope": ["offline_access "], -} -``` - -
diff --git a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-m2m.mdx b/docs/authentication/unified-login/_blocks/create-oauth2-client-request-m2m.mdx deleted file mode 100644 index af751e4862..0000000000 --- a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-m2m.mdx +++ /dev/null @@ -1,121 +0,0 @@ - - -import CreateOAuth2ClientRequestDetails from "./create-oauth2-client-request-details.mdx"; - - - - -```bash -curl --location --request POST '^{coreInfo.uri}/recipe/oauth/clients' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data ' - { - "clientName": "", - "grantTypes": ["client_credentials"], - "scope": " ", - "audience": [""], - } -' -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/recipe/oauth/clients`; - const options = { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - clientName: "", - grantTypes: ["client_credentials"], - scope: " ", - audience: [""], - }) - }; - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); -``` - - - - - ```go - - import ( - "fmt" - "net/http" - "strings" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) - payload := `{ - "clientName": "", - "grantTypes": ["client_credentials"], - "scope": " ", - "audience": [""], - }` - - req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - from typing import Dict, Any - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/recipe/oauth/clients" - - payload: Dict[str, Any] ={ - "clientName": "", - "grantTypes": ["client_credentials"], - "scope": "custom_scope_1> ", - "audience": [""], - } - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - ``` - - - - - - diff --git a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-no-auth.mdx b/docs/authentication/unified-login/_blocks/create-oauth2-client-request-no-auth.mdx deleted file mode 100644 index 2eddde7955..0000000000 --- a/docs/authentication/unified-login/_blocks/create-oauth2-client-request-no-auth.mdx +++ /dev/null @@ -1,129 +0,0 @@ - - -import CreateOAuth2ClientRequestDetails from "./create-oauth2-client-request-details.mdx"; - - - - -```bash -curl --location --request POST '^{coreInfo.uri}/recipe/oauth/clients' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data ' - { - "clientName": "", - "responseTypes": ["code", "id_token"], - "grantTypes": ["authorization_code", "refresh_token"], - "tokenEndpointAuthMethod": "none", - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - } -' -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/recipe/oauth/clients`; - const options = { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - clientName: "", - responseTypes: ["code", "id_token"], - grantTypes: ["authorization_code", "refresh_token"], - tokenEndpointAuthMethod: "none", - scope: "offline_access ", - redirectUris: ["https:///oauth/callback"], - }) - }; - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); -``` - - - - - ```go - - import ( - "fmt" - "net/http" - "strings" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) - payload := `{ - "clientName": "", - "responseTypes": ["code", "id_token"], - "grantTypes": ["authorization_code", "refresh_token"], - "tokenEndpointAuthMethod": "none", - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - }` - - req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - from typing import Dict, Any - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/recipe/oauth/clients" - - payload: Dict[str, Any] ={ - "clientName": "", - "responseTypes": ["code", "id_token"], - "grantTypes": ["authorization_code", "refresh_token"], - "tokenEndpointAuthMethod": "none", - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - } - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - ``` - - - - - - diff --git a/docs/authentication/unified-login/_blocks/create-oauth2-client-request.mdx b/docs/authentication/unified-login/_blocks/create-oauth2-client-request.mdx deleted file mode 100644 index 8f42db0eaa..0000000000 --- a/docs/authentication/unified-login/_blocks/create-oauth2-client-request.mdx +++ /dev/null @@ -1,125 +0,0 @@ - - -import CreateOAuth2ClientRequestDetails from "./create-oauth2-client-request-details.mdx"; - - - - -```bash -curl --location --request POST '^{coreInfo.uri}/recipe/oauth/clients' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data ' - { - "clientName": "", - "responseTypes": ["code"], - "grantTypes": ["authorization_code", "refresh_token"], - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - } -' -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/recipe/oauth/clients`; - const options = { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - clientName: "", - responseTypes: ["code"], - grantTypes: ["authorization_code", "refresh_token"], - scope: "offline_access ", - redirectUris: ["https:///oauth/callback"], - }) - }; - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); -``` - - - - - ```go - - import ( - "fmt" - "net/http" - "strings" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) - payload := `{ - "clientName": "", - "responseTypes": ["code"], - "grantTypes": ["authorization_code", "refresh_token"], - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - }` - - req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - from typing import Dict, Any - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/recipe/oauth/clients" - - payload: Dict[str, Any] ={ - "clientName": "", - "responseTypes": ["code"], - "grantTypes": ["authorization_code", "refresh_token"], - "scope": "offline_access ", - "redirectUris": ["https:///oauth/callback"], - } - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - ``` - - - - - - diff --git a/docs/authentication/unified-login/_blocks/custom-ui-guide.mdx b/docs/authentication/unified-login/_blocks/custom-ui-guide.mdx deleted file mode 100644 index c8b64b910f..0000000000 --- a/docs/authentication/unified-login/_blocks/custom-ui-guide.mdx +++ /dev/null @@ -1,271 +0,0 @@ - - -The user interface that you are going to build should respect this flow: - - - -## A user accesses your application and tries to login. - -It's up to you how you want to handle this. -They can click a button to login or you can directly start the login flow. - -## They get redirected to the **Authorization Service Backend** - -A **OAuth2/OpenID Connect (OIDC)** library can execute this action. -Check the previous guides for information on what you could use. - -## The **Authorization Service Backend** redirects them to the **Authorization Service Frontend** login page. - -The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. -Besides that, the URL can also include a `forceFreshAuth` parameter. -As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. -This guide shows you how to handle this. - -## The **Authorization Service Frontend** renders the login UI and the user performs the login action. - -The login UI should render based on instructions that are specific to each authentication method which you are using. -The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. - -## The **Authorization Service Frontend** redirects the user back to the **Authorization Service Backend** - -After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. -From here, the authentication flow completes. - - - -Let's see how you can actually implement this UI. - -#### 4.1 Configure the redirection URLs - -As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. - -The default values for these routes are: - -- The login page maps to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}` (this is also the place where a user ends up after logout) -- The token refresh page maps to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/try-refresh` -- The logout page maps to `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/logout` - -If you want to change these routes, you need to add a custom override. - -:::info -This override needs addition to the **Authorization Service Backend**. -::: - - - - - -```tsx -import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; - -OAuth2Provider.init({ - override: { - functions: (originalFunctions) => ({ - ...originalFunctions, - getFrontendRedirectionURL: async (input) => { - const websiteDomain = '^{appInfo.websiteDomain}'; - const websiteBasePath = '^{appInfo.websiteBasePath}'; - - if (input.type === "login") { - const queryParams = new URLSearchParams({ - loginChallenge: input.loginChallenge, - }); - if (input.hint !== undefined) { - queryParams.set("hint", input.hint); - } - if (input.forceFreshAuth) { - queryParams.set("forceFreshAuth", "true"); - } - - return `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}?${queryParams.toString()}`; - } else if (input.type === "try-refresh") { - return `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/try-refresh?loginChallenge=${input.loginChallenge}`; - } else if (input.type === "post-logout-fallback") { - return `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}`; - } else if (input.type === "logout-confirmation") { - return `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}/oauth/logout?logoutChallenge=${input.logoutChallenge}`; - } - - return `^{appInfo.websiteDomain}^{appInfo.websiteBasePath}`; - }, - }), - }, -}) - -``` - - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. - -::: - - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Python SDK. - -::: - - - - - -#### 4.2 Handle the forceFreshAuth parameter - -Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. -The `forceFreshAuth` parameter shows this. - -When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. - -Here is an example of how you can evaluate this case. - -```tsx -import Session from 'supertokens-web-js/recipe/session'; - -async function shouldLogin() { - const urlParams = new URLSearchParams(window.location.search); - const forceFreshAuth = urlParams.get('forceFreshAuth') as string; - if(forceFreshAuth === "true") return true; - - return Session.doesSessionExist(); -} - -``` - -:::info Multi Tenancy - -If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. - -::: - -#### 4.3 Complete the login attempt - - -After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2** flow. - -The following code sample shows you how to determine which URL to use. - - - - - -```tsx -import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; - -async function getInitialRedirectionURL() { - const urlParams = new URLSearchParams(window.location.search); - const loginChallenge = urlParams.get('loginChallenge') as string; - const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); - if (redirectionResponse.status === "OK") { - return redirectionResponse.frontendRedirectTo; - } -} - -``` - - - - - -:::caution - -For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. - -::: - - - - - -#### 4.4 Add the token refresh page - -To have support for token refreshing, you need to add a new page to your application. -The path should correspond to the one outlined during the first step. - -When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. -Then they need redirection to a page from your application. - -Here's a code sample that shows you how to do this. - - - - - -```tsx -import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; -import Session from 'supertokens-web-js/recipe/session'; - -async function refreshToken() { - await Session.attemptRefreshingSession(); - const urlParams = new URLSearchParams(window.location.search); - const loginChallenge = urlParams.get('loginChallenge') as string; - const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); - if (redirectionResponse.status === "OK") { - window.location.href = redirectionResponse.frontendRedirectTo; - } -} - -``` - - - - - -:::caution - -For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. - -::: - - - - - -#### 4.5 Add the logout page - -You need to add a logout page that users access when they want to end their session. -The path should correspond to the one outlined during the first step. - -The logout action should first ask the user for confirmation. -If the confirmation passes, then you can call the recipe function. -Based on the final response you can redirect the user to the provided redirection URL. - - - - - -```tsx -import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; - -async function logout() { - const confirmation = confirm("Are you sure that you want to log out?"); - if(!confirmation) return; - - const urlParams = new URLSearchParams(window.location.search); - const logoutChallenge = urlParams.get('logoutChallenge') as string; - const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); - window.location.href = redirectResponse.frontendRedirectTo; -} -``` - - - - - -:::caution - -For mobile apps, you need to reuse the web authentication flow. Check this [guide](/docs/authentication/unified-login/reuse-website-login) for more information. - -::: - - - - diff --git a/docs/authentication/unified-login/_blocks/paid-feature-callout.mdx b/docs/authentication/unified-login/_blocks/paid-feature-callout.mdx deleted file mode 100644 index 55296a7810..0000000000 --- a/docs/authentication/unified-login/_blocks/paid-feature-callout.mdx +++ /dev/null @@ -1,6 +0,0 @@ - -:::warning Paid Feature -This is a paid feature. - -For managed service users, open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and enable this feature from **Features**. Changes are saved automatically. Once enabled, this feature is free on the provided development environment. -::: diff --git a/docs/authentication/unified-login/_category_.json b/docs/authentication/unified-login/_category_.json deleted file mode 100644 index 958f0dc6c7..0000000000 --- a/docs/authentication/unified-login/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Unified Login", - "position": 6 -} diff --git a/docs/authentication/unified-login/add-custom-claims-in-tokens.mdx b/docs/authentication/unified-login/add-custom-claims-in-tokens.mdx index 4e2d2fc15b..a0ed3d5ff4 100644 --- a/docs/authentication/unified-login/add-custom-claims-in-tokens.mdx +++ b/docs/authentication/unified-login/add-custom-claims-in-tokens.mdx @@ -1,18 +1,10 @@ --- title: Add custom claims in tokens -hide_title: true -sidebar_position: 6 description: Add custom claims to OAuth2 access and ID tokens using overrides -page_type: guide -recipe: oauth2 -category: unified-login +sidebar: + order: 6 --- -import PaidFeatureCallout from './_blocks/paid-feature-callout.mdx' - -# Add custom claims in tokens - - ## Overview If you want to add custom properties in the token payloads you can do this by using overrides. @@ -22,56 +14,54 @@ If you want to add custom properties in the token payloads you can do this by us ## Add claims in the OAuth2 Access Token - - - - + + Override the `buildAccessTokenPayload` function to include the custom claims. + + +:::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] +::: + + +Override the `build_access_token_payload` function to include the custom claims. + + + + ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ - override: { - functions: (originalImplementation) => ({ - ...originalImplementation, - buildAccessTokenPayload: async (input) => { - const addedInfo: Record = {}; - if (input.scopes.includes("profile")) { - addedInfo.profile = "custom-value"; - } - return { - ...(await originalImplementation.buildAccessTokenPayload(input)), - ...addedInfo, - }; - }, - }), - }, + override: { + functions: (originalImplementation) => ({ + ...originalImplementation, + buildAccessTokenPayload: async (input) => { + const addedInfo: Record = {}; + if (input.scopes.includes("profile")) { + addedInfo.profile = "custom-value"; + } + return { + ...(await originalImplementation.buildAccessTokenPayload(input)), + ...addedInfo, + }; + }, + }), + }, }); - ``` + + - - - - -:::caution -At the moment there is no support for creating OAuth2 providers in the Go SDK. -::: - - - - - -Override the `build_access_token_payload` function to include the custom claims. - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client -from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface -from supertokens_python.types import User -from typing import Dict, List, Any, Optional +from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface +from supertokens_python.types import User +from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_access_token_payload = original_implementation.build_access_token_payload @@ -86,7 +76,7 @@ def override_oauth2provider_functions(original_implementation: RecipeInterface): added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" - + original_payload = await original_build_access_token_payload( user, client, session_handle, scopes, user_context ) @@ -97,7 +87,7 @@ def override_oauth2provider_functions(original_implementation: RecipeInterface): init( - framework="...", # type: ignore + framework="...", app_info=InputAppInfo( app_name="...", api_domain="...", @@ -114,65 +104,61 @@ init( ], ) ``` - - - - + + --- ## Add claims in the ID Token - - - - + + Override the `buildIdTokenPayload` function to include the custom claims. + + +:::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] +::: + + +Override the `build_id_token_payload` function to include the custom claims. + + + + ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ - override: { - functions: (originalImplementation) => ({ - ...originalImplementation, - buildIdTokenPayload: async (input) => { - const addedInfo: Record = {}; - if (input.scopes.includes("profile")) { - addedInfo.profile = "custom-value"; - } - return { - ...(await originalImplementation.buildIdTokenPayload(input)), - ...addedInfo, - }; - }, - }), - }, + override: { + functions: (originalImplementation) => ({ + ...originalImplementation, + buildIdTokenPayload: async (input) => { + const addedInfo: Record = {}; + if (input.scopes.includes("profile")) { + addedInfo.profile = "custom-value"; + } + return { + ...(await originalImplementation.buildIdTokenPayload(input)), + ...addedInfo, + }; + }, + }), + }, }); - ``` + + - - - - -:::caution -At the moment there is no support for creating OAuth2 providers in the Go SDK. -::: - - - - - -Override the `build_id_token_payload` function to include the custom claims. - -```python + + +```python check=false reason="This example omits surrounding application and SuperTokens configuration." from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider -from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client -from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface -from supertokens_python.types import User -from typing import Dict, List, Any, Optional +from supertokens_python.recipe import oauth2provider +from supertokens_python.recipe.oauth2provider.oauth2_client import OAuth2Client +from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface +from supertokens_python.types import User +from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_build_id_token_payload = original_implementation.build_id_token_payload @@ -187,7 +173,7 @@ def override_oauth2provider_functions(original_implementation: RecipeInterface): added_info = {} if "profile" in scopes: added_info['profile'] = "custom-value" - + original_payload = await original_build_id_token_payload( user, client, session_handle, scopes, user_context ) @@ -198,7 +184,7 @@ def override_oauth2provider_functions(original_implementation: RecipeInterface): init( - framework="...", # type: ignore + framework="...", app_info=InputAppInfo( app_name="...", api_domain="...", @@ -215,7 +201,5 @@ init( ], ) ``` - - - - + + diff --git a/docs/authentication/unified-login/introduction.mdx b/docs/authentication/unified-login/introduction.mdx index 177a615ded..1604f548d2 100644 --- a/docs/authentication/unified-login/introduction.mdx +++ b/docs/authentication/unified-login/introduction.mdx @@ -1,20 +1,10 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -pagination_prev: null -skip_llms_txt: true -description: >- - Authenticate multiple applications using SuperTokens and OAuth2 for unified - login across platforms. -page_type: overview -recipe: oauth2 -category: unified-login +description: Authenticate multiple applications using SuperTokens and OAuth2 for unified login across platforms. +sidebar: + order: 1 --- - -# Unified Login - ## Overview The **Unified Login** feature helps you in scenarios which involve different types of applications using a common **Authorization Server**. @@ -23,9 +13,9 @@ With it you can configure a common **OAuth2 Provider** that authenticates all yo ## Prerequisites - + -Before you can dive deeper in the functionality there are a few things to keep in mind: +Before you can dive deeper in the functionality there are a few things to keep in mind: - The feature is available with the **SuperTokens Managed Service**. It is not included in the **Self-Hosted** version. - You can use it with the `Node.js` or the `Python` backend SDKs. @@ -33,7 +23,7 @@ On `golang` you have to wait for the next releases or configure a separate **Aut - Magic link based login is not supported. However, you can switch to `email`/`SMS` **OTP** instead. This method offers the same level of security. - *Step Up Authentication* is not available out of the box. You have to use customizations to support the flow. -## Getting started +## Getting started Three separate quickstart guides are available for you to follow. They organize the content based on the specific use case that you want to implement. @@ -41,71 +31,34 @@ They organize the content based on the specific use case that you want to implem Before you explore a guide, read through the **OAuth2 Basics** page first. It explains concepts used in each tutorial. - - - - OAuth2 Basics
-
- - Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. - -
- - - Applications with a common backend - - - Implement an authentication flow that involves multiple frontend applications that communicate with a common backend. - - - - - Applications with separate backends - - - Implement an authentication flow that involves multiple frontend applications that communicate with separate backends. - - - - - Common authentication for web and mobile - - - Use a common authentication service for both web and mobile applications. - - -
- - -## Customization - -To adjust the functionality to fit your use case you can explore different sections from the documentation. - - - - - Work with scopes - - - Discover the built-in scopes and see how you can override them. - - - - - Verify Tokens - - - Learn how to validate tokens. - - - - - Add custom claims in tokens - - - Modify the token payload with custom claims. - - - - - + + +Go through a quick summary of the OAuth2 specifications to get accustomed with the language used in the quickstart guides. + + +Implement an authentication flow that involves multiple frontend applications that communicate with a common backend. + + +Implement an authentication flow that involves multiple frontend applications that communicate with separate backends. + + +Use a common authentication service for both web and mobile applications. + + + + +## Customization + +To adjust the functionality to fit your use case you can explore different sections from the documentation. + + + +Discover the built-in scopes and see how you can override them. + + +Learn how to validate tokens. + + +Modify the token payload with custom claims. + + diff --git a/docs/authentication/unified-login/meta.ts b/docs/authentication/unified-login/meta.ts new file mode 100644 index 0000000000..5a9d8a5446 --- /dev/null +++ b/docs/authentication/unified-login/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Unified Login", + icon: "share-2", + order: 60, +}); diff --git a/docs/authentication/unified-login/oauth2-basics.mdx b/docs/authentication/unified-login/oauth2-basics.mdx index 9de64328ca..01d44ab920 100644 --- a/docs/authentication/unified-login/oauth2-basics.mdx +++ b/docs/authentication/unified-login/oauth2-basics.mdx @@ -1,28 +1,17 @@ --- -title: OAuth2 Basics -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 -description: >- - Authenticate multiple applications using SuperTokens and OAuth2 for unified - login across platforms. -page_type: guide -recipe: oauth2 -category: unified-login +title: OAuth 2.0 Basics +description: Authenticate multiple applications using SuperTokens and OAuth 2.0 for unified login across platforms. +sidebar: + order: 2 --- -import PaidFeatureCallout from './_blocks/paid-feature-callout.mdx' -import { Badge, HoverCard } from '@radix-ui/themes'; - -# OAuth2 basics - ## Overview -Each quickstart guide uses OAuth2 specific concepts that you should be aware of. -Read through this page to get a better understanding of the specifications. +Each quickstart guide uses OAuth 2.0-specific concepts that you should be aware of. +Read through this page to get a better understanding of the specifications. -**OAuth2**, Open Authorization, is an industry-standard authorization framework that enables third-party applications to obtain limited access to a user's resources without exposing their credentials. +**OAuth 2.0** is an industry-standard authorization framework that enables applications to obtain limited access to a user's resources without exposing their credentials. OpenID Connect (OIDC) adds an identity layer to OAuth 2.0. ## Terminology @@ -44,6 +33,11 @@ With that token the client can perform authorized operations on behalf of the [* The term **client** does not imply any particular implementation characteristics (for example, whether the application executes on a server, a desktop, or other devices). +OAuth 2.0 distinguishes between two client types: + +- A **confidential client** runs in an environment that can protect credentials, such as an application backend. It can authenticate at the token endpoint with a client secret or another supported method. Never send its secret to a browser or native application. +- A **public client** runs in an environment that cannot protect credentials, such as browser JavaScript or a native application. Configure it with `tokenEndpointAuthMethod: "none"`; it must not have or ship a client secret. Public clients must use the authorization code flow with Proof Key for Code Exchange (PKCE). + #### Resource Server The server hosting the protected resources, capable of accepting and responding to protected resource requests using [**OAuth2 Access Tokens**](#oauth2-access-token). @@ -59,7 +53,7 @@ The server issuing [**OAuth2 Access Tokens**](#oauth2-access-token) to the [**Cl ### Tokens Tokens are strings that represent the authorization issued to the [**Client**](#client). -They are mainly used to access to protected resources, on behalf of the [**Resource Owner**](#resource-owner). +They are mainly used to access protected resources on behalf of the [**Resource Owner**](#resource-owner). At the same time, tokens can provide more information about who the owner is. #### OAuth2 Access Token @@ -67,9 +61,8 @@ At the same time, tokens can provide more information about who the owner is. This is the main token that provides temporary access to protected resources. The **OAuth2 Access Token** should only be accessed and validated by the [**Resource Server**](#resource-server). -:::info -This token is different from the **SuperTokens Session Access Token**. -The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. +:::info[This token is different from the **SuperTokens Session Access Token**.] +The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### OAuth2 Refresh Token @@ -77,9 +70,8 @@ The latter functions in the **OAuth 2.0** authentication flows to maintain a ses A token that allows obtaining a new [**OAuth2 Access Token**](#oauth2-access-token) when the current one has expired. Using the refresh token does not require the user to re-authenticate. -:::info -This token is different from the **SuperTokens Session Refresh Token**. -The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. +:::info[This token is different from the **SuperTokens Session Refresh Token**.] +The latter functions in the **OAuth 2.0** authentication flows to maintain a session between the **authorization frontend** and the **authorization backend server**. ::: #### ID Token @@ -95,7 +87,7 @@ They specify what portions of the **Resource Owner’s** data the **Client** can For example, when a user grants a web application permission to read their email, the application might request the `email` scope. In a general authentication flow scopes get used in the following way: -1. When the **Client** gets created, it configures a series of scopes for the **Authorization Server** +1. When the **Client** gets created, it configures a series of scopes for the **Authorization Server**. 2. The **Authorization Server** authenticates the **Resource Owner** and uses the scopes to generate an **OAuth2 Access Token**. 3. The **Resource Server** checks the scopes of the **OAuth2 Access Token** and only allows the requested actions. @@ -108,37 +100,38 @@ Our implementation supports the following flow types: #### [Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) -Authorization Code Grant +Authorization Code Grant -This flow is best suited for scenarios that involve **web applications**. +This flow is appropriate for confidential web applications and, with PKCE, public browser and native applications. It consists of the following steps: 1. The **Client** redirects the **Resource Owner** to the **Authorization Server’s** authorization endpoint. 2. If the **Resource Owner** grants permission, the **Authorization Server** redirects their browser back to the specified **Redirect URI** and includes an **Authorization Code** as a query parameter. -3. The **Client** then sends a request to the **Authorization Server**’s token endpoint, including the **Authorization Code**. +3. The **Client** then sends a request to the **Authorization Server**’s token endpoint, including the **Authorization Code**. A confidential client authenticates at this endpoint; a public client supplies its PKCE code verifier instead of a client secret. 4. The **Authorization Server** verifies the information sent by the **Client** and, if valid, issues an **OAuth2 Access Token**. 5. The token can make requests to the **Resource Server** to access the protected resources on behalf of the **Resource Owner**. ##### Authorization code An **Authorization Code** is a short-lived code that the [**Authorization Server**](#authorization-server) provides to the [**Client**](#client), via a **Redirect URI**, after authorization approval. -This code then gets exchanged for an [**OAuth2 Access Token**](#oauth2-access-token). -The **Authorization Code** flow enhances security by keeping tokens out of the user-agent and letting the [**Client**](#client) manage the backend communication with the [**Authorization Server**](#authorization-server). +This code then gets exchanged for an [**OAuth2 Access Token**](#oauth2-access-token). +For confidential clients, the **Authorization Code** flow keeps tokens out of the user agent by letting the [**Client's**](#client) backend communicate with the [**Authorization Server**](#authorization-server). Public clients exchange the code directly and must use PKCE. ##### Proof key for code exchange (PKCE) -To prevent cross-site request forgery (CSRF) and code injection attacks, the **Authorization Code flow** can use [**PKCE**](https://oauth.net/2/pkce/). +The **Authorization Code flow** uses [**PKCE**](https://oauth.net/2/pkce/) to bind the authorization request to the token request. PKCE mitigates authorization-code interception and injection. It is mandatory for public browser and native clients and is recommended for confidential clients. + +At the beginning of the authentication flow, the **Client** generates a random *code verifier* and sends its derived code challenge in the authorization request. The client must provide the original verifier during the code exchange, so an intercepted code alone is insufficient. -At the beginning of the authentication flow the **Client** generates a random string called a *code verifier*. -This ensures that, even if the **Authorization Code** gets intercepted, it cannot be exchanged for a token without also including the initial code. +PKCE is not a replacement for request binding. Generate an unpredictable `state`, bind it to the initiating browser transaction, and verify it exactly on callback before exchanging the code. For OIDC, also generate and validate a `nonce`, and validate the ID token's issuer, audience, signature, expiry, and nonce. Do not accept callback parameters that are not bound to the transaction that initiated login. #### [Client credentials](https://oauth.net/2/grant-types/client-credentials/) -Client Credentials Grant +Client Credentials Grant This flow is best suited for **machine-to-machine** (M2M) interactions where there is no end-user. It consists of the following steps: 1. The **Client** authenticates with the **Authorization Server** using its own credentials. -2. The **Authorization Server** verifies the credentials. +2. The **Authorization Server** verifies the credentials. 3. The **Authorization Server** returns an **OAuth2 Access Token**. 4. The **Client** uses the **OAuth2 Access Token** to access protected resources. 5. The **Resource Server** validates the **OAuth2 Access Token**. diff --git a/docs/authentication/unified-login/quickstart-guides/_category_.json b/docs/authentication/unified-login/quickstart-guides/_category_.json deleted file mode 100644 index 1ecc161622..0000000000 --- a/docs/authentication/unified-login/quickstart-guides/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Quickstart Guides", - "position": 3 -} diff --git a/docs/authentication/unified-login/quickstart-guides/meta.ts b/docs/authentication/unified-login/quickstart-guides/meta.ts new file mode 100644 index 0000000000..dcbb6fa4c0 --- /dev/null +++ b/docs/authentication/unified-login/quickstart-guides/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Quickstart Guides", + icon: "zap", + order: 3, +}); diff --git a/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend.mdx b/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend.mdx index 64ada82b5f..30382b7348 100644 --- a/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend.mdx +++ b/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend.mdx @@ -1,54 +1,39 @@ --- title: Multiple frontend domains with a common backend -hide_title: true -sidebar_position: 1 -toc_max_heading_level: 4 -description: >- - Implement OAuth2 authentication for multiple frontend domains using a shared - backend service. -page_type: tutorial -recipe: oauth2 -category: unified-login +description: Implement OAuth2 authentication for multiple frontend domains using a shared backend service. +sidebar: + order: 1 --- -import CreateOAuth2ClientRequest from "../_blocks/create-oauth2-client-request.mdx"; -import CustomUIGuide from "../_blocks/custom-ui-guide.mdx"; - - -# Multiple frontend domains with a common backend - ## Overview -You can implement the following guide if you have multiple **`frontend applications`** that use the same **`backend service`**. +Use this guide when multiple frontend applications call the same backend service. In this topology, each browser application exchanges its own authorization code, so each is a **public OAuth client**. The authentication flow works in the following way: -## The User accesses the `frontend` application: - - The application `frontend` redirects the user to the **Authorization Service** backend, using the authorize URL. + +- The application `frontend` redirects the user to the **Authorization Service** backend, using the authorize URL. - The **Authorization Service** backend redirects the user to the login UI. - -## The User completes the login attempt: - - The **Authorization Service** backend redirects the user to the `callback URL`. - -## The user accesses the callback URL: - - The `frontend` uses the callback URL information to obtain a **OAuth2 Access Token** from the **Authorization Service** backend. - + + +- The **Authorization Service** backend redirects the user to the `callback URL`. + + +- The frontend verifies the callback `state`, then exchanges the Authorization Code with its PKCE code verifier. It never uses a client secret. + -Multiple Frontend Domains with a Single Backend +Multiple Frontend Domains with a Single Backend ## Before you start - - -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page, please follow the tutorial and return here once you're done. + :::info -If your frontend applications are on the same **domain**, but on different **sub-domains**, you can use [Session Sharing Across Subdomains](/docs/post-authentication/session-management/share-session-across-sub-domains). +If your frontend applications are on the same **domain**, but on different **sub-domains**, you can use [Session Sharing Across Subdomains](/post-authentication/session-management/share-session-across-sub-domains). ::: @@ -61,65 +46,269 @@ Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard ### 2. Create the OAuth2 Clients -For each of your **`frontend`** applications create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). -This can occur by directly calling the **SuperTokens Core** API. +For each frontend application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). +Call the **SuperTokens Core** API from a trusted administrative environment. The examples create public clients: `tokenEndpointAuthMethod` is `none`, no secret is issued or shipped, and `allowedCorsOrigins` contains only the exact origin that may call the token endpoint. Each application must use authorization code with PKCE. + + + + + +```bash +curl --location --request POST '/recipe/oauth/clients' \ + --header 'api-key: ' \ + --header 'Content-Type: application/json; charset=utf-8' \ + --data ' + { + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "allowedCorsOrigins": ["https://"], + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + } +' +``` + + +```tsx +const BASE_URL = ""; +const API_KEY = ""; + +const url = `${BASE_URL}/recipe/oauth/clients`; +const options = { + method: "POST", + headers: { + "api-key": API_KEY, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + clientName: "", + responseTypes: ["code"], + grantTypes: ["authorization_code", "refresh_token"], + tokenEndpointAuthMethod: "none", + allowedCorsOrigins: ["https://"], + audience: [""], + scope: "offline_access ", + redirectUris: ["https:///oauth/callback"], + }), +}; + +fetch(url, options) + .then((response) => response.json()) + .then((json) => console.log(json)) + .catch((err) => console.error(err)); +``` + + +```go + +import ( + "fmt" + "net/http" + "strings" + "io" +) + +func main() { + baseUrl := "" + apiKey := "" + url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) + payload := `{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "allowedCorsOrigins": ["https://"], + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + }` + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) +} +``` + + +```python +import requests +from typing import Dict, Any + +BASE_URL = "" +API_KEY = "" + +url = f"{BASE_URL}/recipe/oauth/clients" + +payload: Dict[str, Any] ={ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "allowedCorsOrigins": ["https://"], + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] +} + +headers = { + "api-key": API_KEY, + "Content-Type": "application/json", +} + +response = requests.post(url, json=payload, headers=headers) - +print(response.json()) +``` + + + + +Creates an OAuth2 client +**Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. +## Request +### Body Schema +| Name | Type | Description | Required | Default Value | +|--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | +| `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | +| `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | +| `allowedCorsOrigins` | `array` of `string` | Exact browser origins allowed to call OAuth endpoints. | No | - | +| `audience` | `array` of `string` | Resource-server identifiers allowed in access tokens. | No | - | +| `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | +| `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | +| `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | +| `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | +| `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | +| `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | +| `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | +#### GrantType +- `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. +- `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. +- `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. +#### TokenEndpointAuthMethod +- `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. +- `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. +- `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. +- `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). +#### ResponseType +- `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. +- `id_token`: Indicates that the Client expects an ID Token. +#### Time Duration +A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. +### Example +```bash +curl -X POST /recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -H "api-key: " \ + -d '{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "allowedCorsOrigins": ["https://"], + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + }' +``` +## Response +### 200 +The client has been successfully created. +### Relevant response fields + +The response includes the persisted client configuration, including the fields below. + +| Property | Type | Description | +|-------------|----------------------------------|-----------------------------------------------| +| `clientName` | `string` | The name of the client. | +| `clientId` | `string` | Unique identifier for the client. | +| `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | +| `redirectUris` | `array` of `string` | The URLs used for redirection. | +| `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | +| `scope` | `string` | A space-separated string of scopes that the client can request. | +| `responseTypes` | `array` of `string` | Registered response types. | +| `grantTypes` | `array` of `string` | Registered grant types. | +| `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | +| `allowedCorsOrigins` | `array` of `string` | Exact browser origins allowed to call OAuth endpoints. | +| `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | +#### Example +```json +{ + "clientName": "", + "clientId": "", + "tokenEndpointAuthMethod": "none", + "allowedCorsOrigins": ["https://"], + "audience": [""], + "redirectUris": ["https:///oauth/callback"], + "scope": "offline_access " +} +``` + + -:::caution -You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. -The information is necessary in the next steps. +:::warning[Protect OAuth client credentials] +Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. ::: ### 3. Set up the Authorization Service Backend - #### 3.1 Initialize the OAuth2 recipe - - - + + Update the `supertokens.init` call to include the new recipe. + + +:::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] -```tsx +::: + + + + + +```typescript import supertokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "...", - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - OAuth2Provider.init(), - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` + + - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. - -::: - - - - - -```python + + +```python from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider +from supertokens_python.recipe import emailpassword, oauth2provider init( app_info=InputAppInfo( @@ -133,23 +322,28 @@ init( api_key="..." ), recipe_list=[ - oauth2provider.init() + emailpassword.init(), + oauth2provider.init(), ], ) ``` - - - - + + #### 3.2 Update the CORS configuration Set up the Backend API to allow requests from all the frontend domains. - + + +:::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] - +::: + + + + ```tsx import express from "express"; import cors from "cors"; @@ -157,33 +351,23 @@ import supertokens from "supertokens-node"; import { middleware } from "supertokens-node/framework/express"; const app = express(); - + // Add your actual frontend domains here -const allowedOrigins = ["^{appInfo.websiteDomain}", "", ""]; +const allowedOrigins = ["", "", ""]; -app.use(cors({ - // highlight-start +app.use( + cors({ origin: allowedOrigins, allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], credentials: true, - // highlight-end -})); + }), +); ``` + + - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. - -::: - - - - - + + ```python from supertokens_python import get_all_cors_headers from fastapi import FastAPI @@ -196,425 +380,382 @@ app.add_middleware(get_middleware()) app.add_middleware( CORSMiddleware, - # highlight-start allow_origins=[ - "", "", "" + "", "", "" ], - # highlight-end allow_credentials=True, allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], allow_headers=["Content-Type"] + get_all_cors_headers(), ) ``` - - - - + + #### 3.3 Implement a custom session verification function Given that the backend, the **Authorization Server**, also acts as a **Resource Server** you have to account for this in the session verification process. This is necessary because the flow uses two types of tokens: -- **SuperTokens Session Access Token**: Used during the login and logout. +- **SuperTokens Session Access Token**: Used during the login and logout. - **OAuth2 Access Token**: Used to access protected resources and perform actions that need authorization. Hence the logic should distinguish between these two and prevent errors. - - - +Configure `EXPECTED_ISSUER` from the authorization server discovery document and compare it exactly. The released OAuth2Provider validators verify the signature and expiry; the examples also require the configured client ID, audience, and scopes. `checkDatabase`/`check_database` additionally rejects revoked or otherwise inactive tokens. + + Here is an example of how to implement this in the context of an Express API: + + +:::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] +::: + + + + + ```tsx -import supertokens from "supertokens-node"; +import express, { NextFunction, Request, Response } from "express"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; import Session from "supertokens-node/recipe/session"; -import express, { Request, Response, NextFunction } from 'express'; -import * as jose from "jose"; + +const EXPECTED_CLIENT_ID = ""; +const EXPECTED_AUDIENCE = ""; +const EXPECTED_ISSUER = ""; // Usually /auth +const REQUIRED_SCOPES = [""]; interface RequestWithUserId extends Request { userId?: string; } +function getBearerToken(req: Request): string { + const authorization = req.header("authorization"); + if (authorization === undefined || !authorization.startsWith("Bearer ")) { + throw new Error("Missing bearer token"); + } + return authorization.slice("Bearer ".length); +} + async function verifySession(req: RequestWithUserId, res: Response, next: NextFunction) { - let session = undefined; try { - session = await Session.getSession(req, res, { sessionRequired: false }); - } catch (err) { - if ( - !Session.Error.isErrorFromSuperTokens(err) || - err.type !== Session.Error.TRY_REFRESH_TOKEN - ) { - return next(err); + let session; + try { + session = await Session.getSession(req, res, { sessionRequired: false }); + } catch (error) { + if ( + !Session.Error.isErrorFromSuperTokens(error) || + (error.type !== Session.Error.TRY_REFRESH_TOKEN && error.type !== Session.Error.UNAUTHORISED) + ) { + throw error; + } + } + if (session !== undefined) { + req.userId = session.getUserId(); + return next(); } - } - - // In this case we are dealing with a SuperTokens Session - if (session !== undefined) { - const userId = session.getUserId(); - req.userId = userId; - return next(); - } - // The OAuth2 Access Token needs to be manually extracted and validated - let jwt: string | undefined = undefined; - if (req.headers["authorization"]) { - jwt = req.headers["authorization"].split("Bearer ")[1]; - } - if (jwt === undefined) { - return next(new Error("No JWT found in the request")); - } + const validation = await OAuth2Provider.validateOAuth2AccessToken( + getBearerToken(req), + { + clientId: EXPECTED_CLIENT_ID, + audience: EXPECTED_AUDIENCE, + scopes: REQUIRED_SCOPES, + }, + true, + ); + + if (validation.payload.iss !== EXPECTED_ISSUER || typeof validation.payload.sub !== "string") { + throw new Error("Unexpected OAuth token issuer or subject"); + } - try { - const tokenPayload = await validateToken(jwt, ''); - const userId = tokenPayload.sub; - req.userId = userId; + req.userId = validation.payload.sub; return next(); - } catch (err) { - return next(err); + } catch (error) { + return next(error); } } -const JWKS = jose.createRemoteJWKSet( - new URL("^{appInfo.apiDomain}^{appInfo.apiBasePath}/jwt/jwks.json"), -); - -// This is a basic example on how to validate an OAuth2 Token -// We have a separate page that talks more in depth about the process -async function validateToken(jwt: string, requiredScope: string) { - const { payload } = await jose.jwtVerify(jwt, JWKS, { - requiredClaims: ["stt", "scp", "sub"], - }); - - if (payload.stt !== 1) throw new Error("Invalid token"); - const scopes = payload.scp as string[]; - if (!scopes.includes(requiredScope)) throw new Error("Invalid token"); - - return payload; -} - -// You can then use the function as a middleware for a protected route const app = express(); app.get("/protected", verifySession, async (req, res) => { - // Custom logic + // Custom logic }); ``` + + -For more information on how to verify the **OAuth2 Access Tokens**, please check the [separate guide](/docs/authentication/unified-login/verify-tokens). - - - - - -:::caution - -At the moment, there is no support for creating OAuth2 providers in the Go SDK. - -::: - - - - - -```python -from supertokens_python.recipe.session.syncio import get_session -from supertokens_python.recipe.session.exceptions import SuperTokensSessionError, TryRefreshTokenError + + +```python from fastapi.requests import Request -from typing import List, Optional -import jwt -from jwt import PyJWKClient - - -def verify_session(request: Request): - session = None - try: - session = get_session(request) - except SuperTokensSessionError as err: - if not isinstance(err, TryRefreshTokenError): - raise err - - - # In this case we are dealing with a SuperTokens Session - if session is not None: - return True - - - # The OAuth2 Access Token needs to be manually extracted and validated - jwt = None - auth_header = request.headers.get("authorization") - if auth_header: - # Split the Authorization header and get the token - parts = auth_header.split("Bearer ") - if len(parts) > 1: - jwt = parts[1] - if jwt is None: - raise ValueError("No JWT found in the request") - - validate_token(jwt, ''); - return True - - -def validate_token(token: str, required_scope: str) -> bool: - api_domain = "^{appInfo.apiDomain}" - api_base_path = "^{appInfo.apiBasePath}" - client_id = "" - - jwks_url = f"{api_domain}{api_base_path}/jwt/jwks.json" - jwks_client = PyJWKClient(jwks_url) - - try: - signing_key = jwks_client.get_signing_key_from_jwt(token) - decoded = jwt.decode( - token, - signing_key.key, - algorithms=['RS256'], - options={"require": ["stt", "client_id", "scp"]} - ) - - stt: Optional[int] = decoded.get('stt') - if stt != 1: - return False - - token_client_id: Optional[str] = decoded.get('client_id', None) - if client_id != token_client_id: - return False - - - scopes: List[str] = decoded.get('scp', []) - if required_scope not in scopes: - return False - - return True - except Exception: - return False - # -``` +from supertokens_python.recipe.oauth2provider.interfaces import ( + OAuth2TokenValidationRequirements, +) +from supertokens_python.recipe.oauth2provider.syncio import ( + validate_oauth2_access_token, +) +from supertokens_python.recipe.session.exceptions import ( + SuperTokensSessionError, + TryRefreshTokenError, + UnauthorisedError, +) +from supertokens_python.recipe.session.syncio import get_session - +EXPECTED_CLIENT_ID = "" +EXPECTED_AUDIENCE = "" +EXPECTED_ISSUER = "" # Usually /auth +REQUIRED_SCOPES = [""] + + +def get_bearer_token(request: Request) -> str: + authorization = request.headers.get("authorization") + if authorization is None or not authorization.startswith("Bearer "): + raise ValueError("Missing bearer token") + return authorization.removeprefix("Bearer ") + + +def verify_session(request: Request) -> str: + session = None + try: + session = get_session(request, session_required=False) + except SuperTokensSessionError as error: + if not isinstance(error, (TryRefreshTokenError, UnauthorisedError)): + raise + + if session is not None: + return session.get_user_id() + + validation = validate_oauth2_access_token( + get_bearer_token(request), + OAuth2TokenValidationRequirements( + client_id=EXPECTED_CLIENT_ID, + audience=EXPECTED_AUDIENCE, + scopes=REQUIRED_SCOPES, + ), + check_database=True, + ) + payload = validation.payload + if payload.get("iss") != EXPECTED_ISSUER or not isinstance(payload.get("sub"), str): + raise ValueError("Unexpected OAuth token issuer or subject") + return payload["sub"] +``` + + - + + +For more information on how to verify the **OAuth2 Access Tokens**, please check the [separate guide](/authentication/unified-login/verify-tokens). + + ### 4. Configure the Authorization Service Frontend + - - - - -#### 4.1 Initialize the recipe + - +#### 4.1 Initialize the recipe - - -Add the import statement for the new recipe and update the list of recipe to also include the new initialization. + + +Add the import statement for the new recipe and update the list of recipes to also include the new initialization. + + +Update the `AuthComponent` to include the `OAuth2Provider` recipe. +You need to add a new item in the `recipeList` array. + + +Update the `AuthView` component to include the `OAuth2Provider` recipe. +You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. + + + + ```tsx import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - OAuth2Provider.init() - ] + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` + + +```tsx title="/app/auth/auth.component.ts" +import { init as supertokensUIInit } from "supertokens-auth-react"; +import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; +import { DOCUMENT } from "@angular/common"; + +@Component({ + selector: "app-auth", + template: '
', +}) +export class AuthComponent implements OnDestroy, AfterViewInit { + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + ) {} + + ngAfterViewInit() { + this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); + } -##### Include the pre-built UI in the rendering tree. + ngOnDestroy() { + // Remove the script when the component is destroyed + const script = this.document.getElementById("supertokens-script"); + if (script) { + script.remove(); + } + } + + private loadScript(src: string) { + const script = this.renderer.createElement("script"); + script.type = "text/javascript"; + script.src = src; + script.id = "supertokens-script"; + script.onload = () => { + supertokensUIInit({ + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + // Don't forget to also include the other recipes that you are already using + supertokensUIOAuth2Provider.init(), + ], + }); + }; + this.renderer.appendChild(this.document.body, script); + } +} +``` +
+ +```html +import {init as supertokensUIInit} from "supertokens-auth-react"; import supertokensUIOAuth2Provider from +"supertokens-auth-react/recipe/oauth2provider"; + - + +``` + +
- + + +##### Include the pre-built UI in the rendering tree. + + + + + + ```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; +import React from "react"; +import { BrowserRouter, Routes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the /auth route*/} - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [OAuth2ProviderPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } } ``` - - - - - + + ```tsx -import React from 'react'; +import React from "react"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { - render() { - if (canHandleRoute([OAuth2ProviderPreBuiltUI])) { - // This renders the login UI on the /auth route - return getRoutingComponent([OAuth2ProviderPreBuiltUI]) - } - - return ( - {/*Your app*/} - ); + render() { + if (canHandleRoute([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI]); } -} -``` - - - - - -
- - -Update the `AuthComponent` to include the `OAuth2Provider` recipe. -You need to add a new item in the `recipeList` array. - - -```tsx title="/app/auth/auth.component.ts" - import {init as supertokensUIInit} from "supertokens-auth-react"; - import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; - import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document - ) { } - - ngAfterViewInit() { - this.loadScript('^{jsdeliver_prebuiltui}'); - } - - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById('supertokens-script'); - if (script) { - script.remove(); - } - } - - private loadScript(src: string) { - const script = this.renderer.createElement('script'); - script.type = 'text/javascript'; - script.src = src; - script.id = 'supertokens-script'; - script.onload = () => { - supertokensUIInit({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // Don't forget to also include the other recipes that you are already using - supertokensUIOAuth2Provider.init() - ], - }); - } - this.renderer.appendChild(this.document.body, script); - } + return {/*Your app*/}; } +} ``` - -
- - - - -Update the `AuthView` component to include the `OAuth2Provider` recipe. -You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. - -```tsx - import {init as supertokensUIInit} from "supertokens-auth-react"; - import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; - - - -``` - - - -
+ + + + #### 4.2 Disable network interceptors @@ -629,236 +770,498 @@ Given that in the scenario you are implementing, the **OAuth2 Access Tokens** se The automatic request interception causes conflicts. To prevent this, you need to override the `shouldDoInterceptionBasedOnUrl` function in the `Session.init` call. -:::caution - -The code samples assume that you are using `^{appInfo.apiBasePath}` as the `apiBasePath` for the backend authentication routes. +:::warning[The code samples assume that you are using `/auth` as the `apiBasePath` for the backend authentication routes.] If that is different please adjust them based on your use case. ::: - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: + +This change is in your auth route configuration. + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import Session from "supertokens-auth-react/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - // Interception should be done only for routes that need the SuperTokens Session Tokens - const isAuthApiRoute = urlObj.pathname.startsWith("^{appInfo.apiBasePath}"); - const isOAuth2ApiRoute = urlObj.pathname.startsWith("^{appInfo.apiBasePath}/oauth"); - if (!isAuthApiRoute || isOAuth2ApiRoute) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + // Interception should be done only for routes that need the SuperTokens Session Tokens + const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); + const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); + if (!isAuthApiRoute || isOAuth2ApiRoute) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + +```tsx +// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) +import supertokensUISession from "supertokens-auth-react/recipe/session"; - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - +supertokensUISession.init({ + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); + const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); + if (!isAuthApiRoute || isOAuth2ApiRoute) { + return false; + } + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); +``` + + ```tsx // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) import supertokensUISession from "supertokens-auth-react/recipe/session"; supertokensUISession.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("^{appInfo.apiBasePath}")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); + const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); + if (!isAuthApiRoute || isOAuth2ApiRoute) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + + + +This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("^{appInfo.apiBasePath}")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); + const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); + if (!isAuthApiRoute || isOAuth2ApiRoute) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + +```tsx +import Session from "supertokens-web-js/recipe/session"; - +Session.init({ + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + const isAuthApiRoute = urlObj.pathname.startsWith("/auth"); + const isOAuth2ApiRoute = urlObj.pathname.startsWith("/auth/oauth"); + if (!isAuthApiRoute || isOAuth2ApiRoute) { + return false; + } + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); +``` + + - +The snippets retain interception for SuperTokens authentication routes under `/auth`, but explicitly exclude `/auth/oauth`. OAuth token, introspection, and related protocol requests must not receive SuperTokens session headers or automatic session refresh behavior. -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: +For the other routes, you have full control on how you want to attach the **OAuth2 Access Tokens** to the API calls. -This change is in your auth route configuration. +
+ + + + +The user interface that you are going to build should respect this flow: + + + +It's up to you how you want to handle this. +They can click a button to login or you can directly start the login flow. + + +A **OAuth2/OpenID Connect (OIDC)** library can execute this action. +Check the previous guides for information on what you could use. + + +The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. +Besides that, the URL can also include a `forceFreshAuth` parameter. +As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. +This guide shows you how to handle this. + + +The login UI should render based on instructions that are specific to each authentication method which you are using. +The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. + + +After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. +From here, the authentication flow completes. + + + +Let's see how you can actually implement this UI. + +#### 4.1 Configure the redirection URLs + +As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. + +The default values for these routes are: + +- The login page maps to `/auth` (this is also the place where a user ends up after logout) +- The token refresh page maps to `/auth/try-refresh` +- The logout page maps to `/auth/logout` + +If you want to change these routes, you need to add a custom override. + +:::info[This override needs addition to the **Authorization Service Backend**.] +::: + + + +:::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] + +::: + + + + + ```tsx -// this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUISession from "supertokens-auth-react/recipe/session"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; -supertokensUISession.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("^{appInfo.apiBasePath}")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } - } +OAuth2Provider.init({ + override: { + functions: (originalFunctions) => ({ + ...originalFunctions, + getFrontendRedirectionURL: async (input) => { + const websiteDomain = ""; + const websiteBasePath = "/auth"; + + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); + } + if (input.tenantId !== undefined) { + queryParams.set("tenantId", input.tenantId); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + + return `/auth?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `/auth/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `/auth`; + } else if (input.type === "logout-confirmation") { + return `/auth/oauth/logout?logoutChallenge=${input.logoutChallenge}`; } - } -}) + + return `/auth`; + }, + }), + }, +}); ``` + + -This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + + + +#### 4.2 Handle the forceFreshAuth parameter + +Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. +The `forceFreshAuth` parameter shows this. + +When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. + +Here is an example of how you can evaluate this case. ```tsx import Session from "supertokens-web-js/recipe/session"; -Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("^{appInfo.apiBasePath}")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } - } - } - } -}) +async function shouldLogin() { + const urlParams = new URLSearchParams(window.location.search); + const forceFreshAuth = urlParams.get("forceFreshAuth") as string; + if (forceFreshAuth === "true") return true; + + return !(await Session.doesSessionExist()); +} ``` - +:::info[Multi Tenancy] - +If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. -The code snippet only allows interception for API endpoints that start with `^{appInfo.apiBasePath}`. -This ensures that calls made from the Frontend SDKs continue to use the **SuperTokens Session Tokens**. As a result, the authentication flow ends up working. +::: -For the other routes, you have full control on how you want to attach the **OAuth2 Access Tokens** to the API calls. +#### 4.3 Complete the login attempt -
+After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2.0** flow. - +The following code sample shows you how to determine which URL to use. - - + + +:::warning -### 5. Update the login flow in your frontend applications +For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. -Use a generic OAuth2 library to handle the login flow +::: + + - + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; + +async function getInitialRedirectionURL() { + const urlParams = new URLSearchParams(window.location.search); + const loginChallenge = urlParams.get("loginChallenge") as string; + const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); + if (redirectionResponse.status === "OK") { + return redirectionResponse.frontendRedirectTo; + } +} +``` + + - + + -You can use the [react-oidc-context](https://github.com/authts/react-oidc-context) library. -Follow the instructions from the library's page. -Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. -- `authority` corresponds to the endpoint of the **Authorization Service** `^{appInfo.apiDomain}^{appInfo.apiBasePath}` -- `clientID` corresponds to `clientId` -- `redirectUri` corresponds to a value from `callbackUrls` -- `scope` corresponds to `scope` - -If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. -To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. +#### 4.4 Add the token refresh page - +To have support for token refreshing, you need to add a new page to your application. +The path should correspond to the one outlined during the first step. - +When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. +Then they need redirection to a page from your application. -You can use the [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) library. -Follow the instructions described in the [GitHub repository](https://github.com/manfredsteyer/angular-oauth2-oidc?tab=readme-ov-file#logging-in). -Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. +Here's a code sample that shows you how to do this. -- `issuer` corresponds to the endpoint of the **Authorization Service** `^{appInfo.apiDomain}^{appInfo.apiBasePath}` -- `client_id` corresponds to `clientId` -- `redirect_uri` corresponds to a value from `callbackUrls` -- `scope` corresponds to `scope` -If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. -To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. - + + +:::warning + +For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; +import Session from "supertokens-web-js/recipe/session"; + +async function refreshToken() { + await Session.attemptRefreshingSession(); + const urlParams = new URLSearchParams(window.location.search); + const loginChallenge = urlParams.get("loginChallenge") as string; + const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); + if (redirectionResponse.status === "OK") { + window.location.href = redirectionResponse.frontendRedirectTo; + } +} +``` + + + + + + + + +#### 4.5 Add the logout page + +You need to add a logout page that users access when they want to end their session. +The path should correspond to the one outlined during the first step. + +The logout action should first ask the user for confirmation. +If the confirmation passes, then you can call the recipe function. +Based on the final response you can redirect the user to the provided redirection URL. + + + + + +:::warning + +For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; + +async function logout() { + const confirmation = confirm("Are you sure that you want to log out?"); + if (!confirmation) return; + + const urlParams = new URLSearchParams(window.location.search); + const logoutChallenge = urlParams.get("logoutChallenge") as string; + const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); + window.location.href = redirectResponse.frontendRedirectTo; +} +``` + + + + + + - + + + +### 5. Update the login flow in your frontend applications + +Use an OAuth 2.0/OIDC library that supports authorization code with PKCE. For every login: + +1. Generate a fresh high-entropy `state` and PKCE verifier; persist them only for the initiating browser transaction. +2. Send the derived S256 code challenge in the authorization request. +3. On callback, verify `state` exactly before exchanging the code with the verifier. If requesting `openid`, also generate and validate `nonce` and validate the ID token. +4. Keep access and refresh tokens in memory where possible. Do not place them in `localStorage`, browser-readable cookies, or URLs. A backend-for-frontend that stores tokens server-side and issues an opaque `HttpOnly`, `Secure`, appropriately `SameSite` session cookie offers stronger protection against token theft. + + + + +You can use the [react-oidc-context](https://github.com/authts/react-oidc-context) library. +Follow the instructions from the library's page. +Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. +- `authority` corresponds to the endpoint of the **Authorization Service** `/auth` +- `client_id` corresponds to `clientId` +- `redirect_uri` corresponds to a value from `redirectUris` +- `scope` corresponds directly to the space-separated `scope` value +- Set `response_type` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). +If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. +To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "`<TENANT_ID>`" }`. + + +You can use the [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc) library. +Follow the instructions described in the [GitHub repository](https://github.com/manfredsteyer/angular-oauth2-oidc?tab=readme-ov-file#logging-in). +Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. +- `issuer` corresponds to the endpoint of the **Authorization Service** `/auth` +- `clientId` corresponds to `clientId` +- `redirectUri` corresponds to a value from `redirectUris` +- `scope` corresponds directly to the space-separated `scope` value +- Set `responseType` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). +If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. +To do this, set `customQueryParams` to `{ tenant_id: "`<TENANT_ID>`" }`. + + You can use the [oidc-client-ts](https://github.com/authts/oidc-client-ts?tab=readme-ov-file) library. -Follow the instructions described in the [GitHub repository](https://github.com/authts/oidc-client-ts/blob/main/docs/protocols/authorization-code-grant-with-pkce.md). +Follow the instructions described in the [GitHub repository](https://github.com/authts/oidc-client-ts/blob/main/docs/protocols/authorization-code-grant-with-pkce). Identify the configuration parameters based on the response received on **step 2**, when creating the **OAuth2 Client**. - -- `issuer` corresponds to the endpoint of the **Authorization Service** `^{appInfo.apiDomain}^{appInfo.apiBasePath}` +- `authority` corresponds to the endpoint of the **Authorization Service** `/auth` - `client_id` corresponds to `clientId` -- `redirect_uri` corresponds to a value from `callbackUrls` -- `scope` corresponds to `scope` - +- `redirect_uri` corresponds to a value from `redirectUris` +- `scope` corresponds directly to the space-separated `scope` value +- Set `response_type` to `"code"`. The library uses S256 PKCE for code flow and generates and validates `state` (and `nonce` when using OIDC). If you are using a multi-tenant setup, you also need to specify the `tenantId` parameter in the authorization URL. -To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "" }`. - - - - +To do this, set the `extraQueryParams` property with a specific value that should look like this: `{ tenant_id: "`<TENANT_ID>`" }`. + + :::info -If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. +If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: diff --git a/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends.mdx b/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends.mdx index d0f2b01ca9..6e2b337081 100644 --- a/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends.mdx +++ b/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends.mdx @@ -1,56 +1,43 @@ --- title: Multiple frontend domains with separate backends -hide_title: true -sidebar_position: 2 -description: >- - Set up multiple frontend domains with separate backends using OAuth2 - authentication. -page_type: tutorial -recipe: oauth2 -category: unified-login +description: Set up multiple frontend domains with separate backends using OAuth2 authentication. +sidebar: + order: 2 --- - -import CustomUIGuide from "../_blocks/custom-ui-guide.mdx"; -import CreateOAuth2ClientRequest from "../_blocks/create-oauth2-client-request.mdx"; - -# Multiple frontend domains with separate backends - ## Overview -You can use the following guide if you have a single [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) that multiple applications use. +You can use the following guide if you have a single [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) that multiple applications use. In turn, each app has separate **`frontend`** and **`backend`** instances that serve from different domains. The authentication flow works in the following way: - ## The User accesses the `frontend` app - - The application `frontend` calls a login endpoint on the `backend` application. - - The `backend` application generates an `authorization` URL to the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) and redirects the user to it. - - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the login UI - - ## The User completes the login attempt - - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to a `callback URL` that includes the **Authorization Code**. - - ## The user accesses the callback URL - - The **Authentication Code** gets sent to the application `backend` - - The `backend` exchanges the **Authentication Code** for an [**OAuth2 Access Token**](/docs/authentication/unified-login/oauth2-basics#oauth2-access-token) - - The `backend` saves the received token in a server session and sends it back to the `frontend` as a cookie. + +- The application `frontend` calls a login endpoint on the `backend` application. +- The `backend` application generates an `authorization` URL to the [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) and redirects the user to it. +- The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the login UI + + +- The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to a `callback URL` that includes the **Authorization Code**. + + +- The Authorization Code and `state` are sent to the application backend. +- The backend verifies `state`, exchanges the Authorization Code, and keeps the OAuth tokens server-side. +- The backend rotates the application session and sends only an opaque session identifier in a cookie. + -The `frontend` can use the new cookie to access protected resources from the `backend`. +The frontend uses an opaque `HttpOnly`, `Secure`, appropriately `SameSite` application-session cookie to access its backend. OAuth access and refresh tokens never enter browser-readable storage. -Multiple Frontend Domains with separate Backends +Multiple Frontend Domains with separate Backends ## Before you start - - -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page, please follow the tutorial and return here once you're done. + :::info -Note that, if the *frontends* and *backends* are in different *sub domains*, you don't need to use *OAuth* and can instead use [session sharing across sub domains](/docs/post-authentication/session-management/share-session-across-sub-domains). +Note that, if the *frontends* and *backends* are in different *subdomains*, you don't need to use *OAuth* and can instead use [session sharing across sub domains](/post-authentication/session-management/share-session-across-sub-domains). ::: @@ -64,67 +51,278 @@ Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard ### 2. Create the OAuth2 Clients -For each of your applications you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). -You can do this by directly calling the **SuperTokens Core** API. +For each application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). +Call the **SuperTokens Core** API from a trusted administrative environment. Because each application backend performs the code exchange and can protect credentials, these are **confidential clients**. The examples below register `client_secret_basic`, which is appropriate for Go oauth2, `Authlib`, League OAuth2 Client with `HttpBasicAuthOptionProvider`, Spring Security, and ASP.NET Core. Never expose a client secret to frontend code, logs, URLs, or browser storage. +:::warning[passport-oauth2 requires a separately registered client] +passport-oauth2 1.8.0 sends `client_id` and `client_secret` in the token request body. For the Node.js Passport application, register its own client with `tokenEndpointAuthMethod: "client_secret_post"` instead of the `client_secret_basic` value shown below. Do not reuse that client or secret in another application. +::: - -:::caution -You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. -The information is necessary for the next steps. -::: + + + + +```bash +curl --location --request POST '/recipe/oauth/clients' \ + --header 'api-key: ' \ + --header 'Content-Type: application/json; charset=utf-8' \ + --data ' + { + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "client_secret_basic", + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + } +' +``` + + +```tsx +const BASE_URL = ""; +const API_KEY = ""; + +const url = `${BASE_URL}/recipe/oauth/clients`; +const options = { + method: "POST", + headers: { + "api-key": API_KEY, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + clientName: "", + responseTypes: ["code"], + grantTypes: ["authorization_code", "refresh_token"], + tokenEndpointAuthMethod: "client_secret_basic", + audience: [""], + scope: "offline_access ", + redirectUris: ["https:///oauth/callback"], + }), +}; + +fetch(url, options) + .then((response) => response.json()) + .then((json) => console.log(json)) + .catch((err) => console.error(err)); +``` + + +```go + +import ( + "fmt" + "net/http" + "strings" + "io" +) +func main() { + baseUrl := "" + apiKey := "" + url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) + payload := `{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "client_secret_basic", + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + }` -### 3. Set Up your Authorization Service backend + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) -In your [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) you need to initialize the **OAuth2Provider** recipe. -The recipe exposes the endpoints needed for enabling the [**OAuth 2.0**](/docs/authentication/unified-login/oauth2-basics) flow. + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") - - + res, _ := http.DefaultClient.Do(req) -Update the `supertokens.init` call to include the `OAuth2Provider` recipe. + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) -Add the import statement for the recipe and update the list of recipes with the new initialization step. + fmt.Println(string(body)) +} +``` + + +```python +import requests +from typing import Dict, Any + +BASE_URL = "" +API_KEY = "" + +url = f"{BASE_URL}/recipe/oauth/clients" + +payload: Dict[str, Any] ={ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "client_secret_basic", + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] +} -```tsx -import supertokens from "supertokens-node"; -import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +headers = { + "api-key": API_KEY, + "Content-Type": "application/json", +} -supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "...", - }, - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - OAuth2Provider.init(), - ] -}); +response = requests.post(url, json=payload, headers=headers) + +print(response.json()) +``` + + + + + + + + + + + + + +Creates an OAuth2 client +**Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. +## Request +### Body Schema +| Name | Type | Description | Required | Default Value | +|--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | +| `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | +| `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | +| `audience` | `array` of `string` | Resource-server identifiers allowed in access tokens. | No | - | +| `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | +| `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | +| `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | +| `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | +| `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | +| `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | +| `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | +#### GrantType +- `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. +- `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. +- `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. +#### TokenEndpointAuthMethod +- `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. +- `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. +- `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. +- `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). +#### ResponseType +- `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. +- `id_token`: Indicates that the Client expects an ID Token. +#### Time Duration +A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. +### Example +```bash +curl -X POST /recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -H "api-key: " \ + -d '{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "client_secret_basic", + "audience": [""], + "scope": "offline_access ", + "redirectUris": ["https:///oauth/callback"] + }' +``` +## Response +### 200 +The client has been successfully created. +### Relevant response fields + +The response includes the persisted client configuration, including the fields below. + +| Property | Type | Description | +|-------------|----------------------------------|-----------------------------------------------| +| `clientName` | `string` | The name of the client. | +| `clientId` | `string` | Unique identifier for the client. | +| `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | +| `redirectUris` | `array` of `string` | The URLs used for redirection. | +| `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | +| `scope` | `string` | A space-separated string of scopes that the client can request. | +| `responseTypes` | `array` of `string` | Registered response types. | +| `grantTypes` | `array` of `string` | Registered grant types. | +| `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | +| `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | +#### Example +```json +{ + "clientName": "", + "clientId": "", + "clientSecret": "", + "tokenEndpointAuthMethod": "client_secret_basic", + "audience": [""], + "redirectUris": ["https:///oauth/callback"], + "scope": "offline_access " +} ``` + + + +:::warning[Protect OAuth client credentials] +Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. +::: - - +### 3. Set up your Authorization Service backend -:::caution +In your [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) you need to initialize the **OAuth2Provider** recipe. +The recipe exposes the endpoints needed for enabling the [**OAuth 2.0**](/authentication/unified-login/oauth2-basics) flow. -At the moment there is no support for creating OAuth2 providers in the Go SDK. + + +Update the `supertokens.init` call to include the `OAuth2Provider` recipe. + +Add the import statement for the recipe and update the recipe list with the new initialization step. + + +:::warning[At the moment there is no support for creating OAuth2 providers in the Go SDK.] ::: + + - + + +```typescript +import supertokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; - +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [EmailPassword.init(), OAuth2Provider.init()], +}); +``` + + -```python + + +```python from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider +from supertokens_python.recipe import emailpassword, oauth2provider init( app_info=InputAppInfo( @@ -138,354 +336,1013 @@ init( api_key="..." ), recipe_list=[ - oauth2provider.init() + emailpassword.init(), + oauth2provider.init(), ], ) ``` + + - - - + + -### 4. Configure the Authorization Service frontend + + + + - +### 4. Configure the Authorization Service frontend - -#### 4.1 Initialize the recipe + - + - +#### 4.1 Initialize the recipe -Add the import statement for the new recipe and update the list of recipe to also include the new initialization. + + +Add the import statement for the new recipe and update the list of recipes to also include the new initialization. + + +Update the `AuthComponent` to include the `OAuth2Provider` recipe. +You need to add a new item in the `recipeList` array. + + +Update the `AuthView` component to include the `OAuth2Provider` recipe. +You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. + + + + ```tsx import OAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - appName: "...", - apiDomain: "...", - websiteDomain: "...", - }, - recipeList: [ - OAuth2Provider.init() - ] + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [EmailPassword.init(), OAuth2Provider.init()], }); ``` + + +```tsx title="/app/auth/auth.component.ts" +import { init as supertokensUIInit } from "supertokens-auth-react"; +import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; +import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; +import { DOCUMENT } from "@angular/common"; + +@Component({ + selector: "app-auth", + template: '
', +}) +export class AuthComponent implements OnDestroy, AfterViewInit { + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + ) {} + + ngAfterViewInit() { + this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js"); + } -#### 4.2 Include the pre-built UI in the rendering tree. + ngOnDestroy() { + // Remove the script when the component is destroyed + const script = this.document.getElementById("supertokens-script"); + if (script) { + script.remove(); + } + } + + private loadScript(src: string) { + const script = this.renderer.createElement("script"); + script.type = "text/javascript"; + script.src = src; + script.id = "supertokens-script"; + script.onload = () => { + supertokensUIInit({ + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + // Don't forget to also include the other recipes that you are already using + supertokensUIOAuth2Provider.init(), + ], + }); + }; + this.renderer.appendChild(this.document.body, script); + } +} +``` +
+ +```html +import {init as supertokensUIInit} from "supertokens-auth-react"; import supertokensUIOAuth2Provider from +"supertokens-auth-react/recipe/oauth2provider"; + - + +``` + +
- + + +#### 4.2 Include the pre-built UI in the rendering tree. + + + + + + ```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; +import React from "react"; +import { BrowserRouter, Routes } from "react-router-dom"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import * as reactRouterDom from "react-router-dom"; class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the /auth route*/} - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [OAuth2ProviderPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } } ``` - - - - - + + ```tsx -import React from 'react'; +import React from "react"; import { OAuth2ProviderPreBuiltUI } from "supertokens-auth-react/recipe/oauth2provider/prebuiltui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; class App extends React.Component { - render() { - if (canHandleRoute([OAuth2ProviderPreBuiltUI])) { - // This renders the login UI on the /auth route - return getRoutingComponent([OAuth2ProviderPreBuiltUI]) - } - - return ( - {/*Your app*/} - ); + render() { + if (canHandleRoute([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([EmailPasswordPreBuiltUI, OAuth2ProviderPreBuiltUI]); } + + return {/*Your app*/}; + } } ``` + + + + - +
- - - + - +The user interface that you are going to build should respect this flow: -Update the `AuthComponent` to include the `OAuth2Provider` recipe. -You need to add a new item in the `recipeList` array. + + +It's up to you how you want to handle this. +They can click a button to login or you can directly start the login flow. + + +A **OAuth2/OpenID Connect (OIDC)** library can execute this action. +Check the previous guides for information on what you could use. + + +The page URL contains a `loginChallenge` parameter that keeps track of the login attempt. +Besides that, the URL can also include a `forceFreshAuth` parameter. +As the name suggests, this should force the login UI to be visible even though the user has an existing valid session. +This guide shows you how to handle this. + + +The login UI should render based on instructions that are specific to each authentication method which you are using. +The additional thing that you have to do here is to consider the `forceFreshAuth` parameter. + + +After the user submits the login form, you need to redirect them to a specific route that sends them to the original application. +From here, the authentication flow completes. + + +Let's see how you can actually implement this UI. -```tsx title="/app/auth/auth.component.ts" - import {init as supertokensUIInit} from "supertokens-auth-react"; - import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; - import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document - ) { } - - ngAfterViewInit() { - this.loadScript('^{prebuiltUIVersion}'); - } +#### 4.1 Configure the redirection URLs - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById('supertokens-script'); - if (script) { - script.remove(); - } - } +As it has hinted in the previous section, the **Authorization Service Backend** sends the user to different pages from the **Authorization Service Frontend**, based on the action that needs execution. - private loadScript(src: string) { - const script = this.renderer.createElement('script'); - script.type = 'text/javascript'; - script.src = src; - script.id = 'supertokens-script'; - script.onload = () => { - supertokensUIInit({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" - }, - recipeList: [ - // Don't forget to also include the other recipes that you are already using - supertokensUIOAuth2Provider.init() - ], - }); +The default values for these routes are: + +- The login page maps to `/auth` (this is also the place where a user ends up after logout) +- The token refresh page maps to `/auth/try-refresh` +- The logout page maps to `/auth/logout` + +If you want to change these routes, you need to add a custom override. + +:::info[This override needs addition to the **Authorization Service Backend**.] +::: + + + +:::warning[At the moment, there is no support for creating OAuth2 providers in the Go SDK.] + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; + +OAuth2Provider.init({ + override: { + functions: (originalFunctions) => ({ + ...originalFunctions, + getFrontendRedirectionURL: async (input) => { + const websiteDomain = ""; + const websiteBasePath = "/auth"; + + if (input.type === "login") { + const queryParams = new URLSearchParams({ + loginChallenge: input.loginChallenge, + }); + if (input.hint !== undefined) { + queryParams.set("hint", input.hint); } - this.renderer.appendChild(this.document.body, script); - } - } + if (input.tenantId !== undefined) { + queryParams.set("tenantId", input.tenantId); + } + if (input.forceFreshAuth) { + queryParams.set("forceFreshAuth", "true"); + } + + return `/auth?${queryParams.toString()}`; + } else if (input.type === "try-refresh") { + return `/auth/try-refresh?loginChallenge=${input.loginChallenge}`; + } else if (input.type === "post-logout-fallback") { + return `/auth`; + } else if (input.type === "logout-confirmation") { + return `/auth/oauth/logout?logoutChallenge=${input.logoutChallenge}`; + } + + return `/auth`; + }, + }), + }, +}); ``` + + + + -
+ + - +#### 4.2 Handle the forceFreshAuth parameter +Sometimes, even though there is an existing valid session in the **Authorization Service Frontend**, the requesting **Client** might force a new login attempt. +The `forceFreshAuth` parameter shows this. -Update the `AuthView` component to include the `OAuth2Provider` recipe. -You need to add a new item in the `recipeList` array, inside the `supertokensUIInit` call. +When the login page renders, you also need to check for this parameter. You are doing this to know if you need to show the login UI. + +Here is an example of how you can evaluate this case. ```tsx - import {init as supertokensUIInit} from "supertokens-auth-react"; - import supertokensUIOAuth2Provider from "supertokens-auth-react/recipe/oauth2provider"; - +import Session from "supertokens-web-js/recipe/session"; + +async function shouldLogin() { + const urlParams = new URLSearchParams(window.location.search); + const forceFreshAuth = urlParams.get("forceFreshAuth") as string; + if (forceFreshAuth === "true") return true; - + return !(await Session.doesSessionExist()); +} ``` +:::info[Multi Tenancy] + +If you are using multi-tenancy, you also need to keep track of the `tenantId` query parameter and pass it between the **Authorization Service Frontend** pages. + +::: + +#### 4.3 Complete the login attempt + + +After the user submits the login form, you need to redirect them to a specific route to complete the **OAuth 2.0** flow. + +The following code sample shows you how to determine which URL to use. + + - - -
+ + +:::warning -
+For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; + +async function getInitialRedirectionURL() { + const urlParams = new URLSearchParams(window.location.search); + const loginChallenge = urlParams.get("loginChallenge") as string; + const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); + if (redirectionResponse.status === "OK") { + return redirectionResponse.frontendRedirectTo; + } +} +``` + + - - - + + + + + +#### 4.4 Add the token refresh page + +To have support for token refreshing, you need to add a new page to your application. +The path should correspond to the one outlined during the first step. + +When the user ends up on this page, you need to use the `Session` recipe to perform the refresh action. +Then they need redirection to a page from your application. + +Here's a code sample that shows you how to do this. + + + + + +:::warning + +For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; +import Session from "supertokens-web-js/recipe/session"; + +async function refreshToken() { + await Session.attemptRefreshingSession(); + const urlParams = new URLSearchParams(window.location.search); + const loginChallenge = urlParams.get("loginChallenge") as string; + const redirectionResponse = await OAuth2Provider.getRedirectURLToContinueOAuthFlow({ loginChallenge }); + if (redirectionResponse.status === "OK") { + window.location.href = redirectionResponse.frontendRedirectTo; + } +} +``` + + + + + + + + +#### 4.5 Add the logout page + +You need to add a logout page that users access when they want to end their session. +The path should correspond to the one outlined during the first step. + +The logout action should first ask the user for confirmation. +If the confirmation passes, then you can call the recipe function. +Based on the final response you can redirect the user to the provided redirection URL. + + + + + +:::warning + +For mobile apps, you need to reuse the web authentication flow. Check this [guide](/authentication/unified-login/quickstart-guides/reuse-website-login) for more information. + +::: + + + + + +```tsx +import OAuth2Provider from "supertokens-web-js/recipe/oauth2provider"; + +async function logout() { + const confirmation = confirm("Are you sure that you want to log out?"); + if (!confirmation) return; + + const urlParams = new URLSearchParams(window.location.search); + const logoutChallenge = urlParams.get("logoutChallenge") as string; + const redirectResponse = await OAuth2Provider.logOut({ logoutChallenge }); + window.location.href = redirectResponse.frontendRedirectTo; +} +``` + + + + + + + + ### 5. Set up session handling in each application In each of your individual `applications` you need to set up logic for handling the **OAuth 2.0** authentication flow. -You can use a generic **OIDC** or **OAuth2** library to do this. +Use a framework OAuth 2.0/OIDC login middleware rather than implementing the protocol manually. The authorization endpoint is `/auth/oauth/auth`, and the token endpoint is `/auth/oauth/token`. Each registered callback must exactly match one of the client's `redirectUris`. + +A secure implementation must: + +1. Generate a high-entropy `state`, bind it to the initiating browser session, and verify it exactly before code exchange. Use PKCE as defense in depth where the library supports it. +2. For OIDC, request `openid`, generate and verify `nonce`, and validate the ID token signature, issuer, audience, expiry, and nonce. +3. Keep OAuth access and refresh tokens in a server-side session store. After callback, rotate the application session identifier to prevent session fixation. +4. Return only an opaque session identifier in an `HttpOnly`, `Secure`, appropriately `SameSite` cookie. Never return OAuth tokens in browser-readable cookies, JavaScript storage, or URLs. +5. Preserve the selected `tenantId` through authorization, callback, and the resulting application session. + + + +With [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/), state protection and PKCE are opt-in. Configure both. Install server-side Express session middleware before Passport; do not use a client-side cookie session store. This example uses the separately registered `client_secret_post` client described in step 2. + +```javascript +const CLIENT_ID = ""; +const EXPECTED_AUDIENCE = ""; +const EXPECTED_ISSUER = ""; +const REQUIRED_SCOPES = [""]; +const INTROSPECTION_URL = "/auth/oauth/introspect"; + +class SuperTokensOAuth2Strategy extends OAuth2Strategy { + authorizationParams(options) { + return { tenant_id: options.tenantId }; + } +} - +async function introspectAndValidateTenant(accessToken, expectedTenant) { + const response = await fetch(INTROSPECTION_URL, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ token: accessToken, scope: REQUIRED_SCOPES.join(" ") }), + }); + if (!response.ok) throw new Error("OAuth introspection failed"); + + const tokenInfo = await response.json(); + const audiences = Array.isArray(tokenInfo.aud) ? tokenInfo.aud : [tokenInfo.aud]; + if ( + tokenInfo.active !== true || + tokenInfo.tId !== expectedTenant || + tokenInfo.client_id !== CLIENT_ID || + tokenInfo.iss !== EXPECTED_ISSUER || + !audiences.includes(EXPECTED_AUDIENCE) + ) { + throw new Error("OAuth token does not match the login transaction"); + } +} - +app.use( + session({ + secret: mustGetEnv("APPLICATION_SESSION_SECRET"), + store: serverSideSessionStore, + resave: false, + saveUninitialized: false, + cookie: { httpOnly: true, secure: true, sameSite: "lax" }, + }), +); +app.use(passport.initialize()); +app.use(passport.session()); + +passport.use( + new SuperTokensOAuth2Strategy( + { + authorizationURL: "/auth/oauth/auth", + tokenURL: "/auth/oauth/token", + clientID: CLIENT_ID, + clientSecret: mustGetEnv("OAUTH_CLIENT_SECRET"), + callbackURL: "https:///oauth/callback", + scope: "offline_access ", + state: true, + pkce: true, + passReqToCallback: true, + }, + async (req, accessToken, refreshToken, params, profile, done) => { + try { + const transaction = req.session.oauthTransaction; + delete req.session.oauthTransaction; + if (transaction === undefined) throw new Error("OAuth transaction missing"); + + await introspectAndValidateTenant(accessToken, transaction.tenantId); + const user = await resolveUserFromOAuthTokens(accessToken, refreshToken); + done(null, user, { + oauthTokens: { accessToken, refreshToken }, + tenantId: transaction.tenantId, + }); + } catch (error) { + done(error); + } + }, + ), +); -You can use the [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/) library. -Follow the instructions on the library's page and set up your application `backend`. -You can determine the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. +app.get("/login", (req, res, next) => { + const tenantId = resolveAllowedTenant(req); + req.session.oauthTransaction = { tenantId }; + passport.authenticate("oauth2", { tenantId })(req, res, next); +}); -- `authorizationURL` corresponds to `authorizeUrl` -- `tokenURL` corresponds to `tokenFetchUrl` -- `clientID` corresponds to `clientId` -- `clientSecret` corresponds to `clientSecret` -- `callbackURL` corresponds to a value from `callbackUrls` -- `scope` corresponds to `scope` +app.get("/oauth/callback", (req, res, next) => { + passport.authenticate("oauth2", { session: false }, (error, user, info) => { + if (error || !user) return next(error ?? new Error("OAuth login failed")); + if (!info?.oauthTokens || typeof info.tenantId !== "string") { + return next(new Error("OAuth transaction result missing")); + } -Make sure that you expose an endpoint that calls `passport.authenticate('oauth2')`. -This way the user ends up accessing the actual login page served by the **Authorization Service**. + req.session.regenerate((regenerateError) => { + if (regenerateError) return next(regenerateError); + req.logIn(user, (loginError) => { + if (loginError) return next(loginError); + applicationSessionStore.set(req.sessionID, { + tenantId: info.tenantId, + oauthTokens: info.oauthTokens, + }); + res.redirect("/"); + }); + }); + })(req, res, next); +}); +``` - +passport-oauth2 generates and consumes its state and PKCE verifier in the initiating `req.session`. The separate `oauthTransaction` binds the allowlisted tenant to that same session, and `authorizationParams` sends it as the released `tenant_id` authorization parameter. The trusted introspection endpoint validates signature, expiry, revocation, and required scopes before the example compares the released `tId`, issuer, client, and audience fields. The rotated opaque application session stores the validated tenant and tokens server-side. + + +Use [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) with a one-time, server-side transaction store. The store must key each transaction by the initiating application session ID; `Consume` must atomically read and delete it. + +```go +import ( + "context" + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" +) - +type OAuthTransaction struct { + State string + Verifier string + TenantID string +} -You can use the [OAuth2](https://pkg.go.dev/golang.org/x/oauth2) library. -Follow these [instructions](https://golang.org/pkg/golang.org/x/oauth2/#example-Config-RequestToken) and implement it in your `backend`. +type Audience []string -You can determine the configuration parameters based on the response received in **step 2**. -- `ClientID` corresponds to `clientId` -- `ClientSecret` corresponds to `clientSecret` -- `Scopes` corresponds to `scope` -- `Endpoint.AuthURL` corresponds to `authorizeUrl` -- `Endpoint.TokenURL` corresponds to `tokenFetchUrl` +func (audience *Audience) UnmarshalJSON(data []byte) error { + var single string + if err := json.Unmarshal(data, &single); err == nil { + *audience = Audience{single} + return nil + } -Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `AuthCodeURL`. -This way the user ends up accessing the actual login page served by the **Authorization Service**. + var multiple []string + if err := json.Unmarshal(data, &multiple); err != nil { + return errors.New("invalid OAuth audience") + } + *audience = multiple + return nil +} - +func (audience Audience) Contains(expected string) bool { + for _, value := range audience { + if value == expected { + return true + } + } + return false +} - +type IntrospectionResponse struct { + Active bool `json:"active"` + Audience Audience `json:"aud"` + TenantID string `json:"tId"` + ClientID string `json:"client_id"` + Issuer string `json:"iss"` +} -You can use the [AuthLib](https://docs.authlib.org/) library. -Follow these [instructions](https://docs.authlib.org/en/latest/client/oauth2.html) and implement it in your `backend`. +type AppSession struct { + TenantID string + Token *oauth2.Token +} -You can determine the configuration parameters based on the response received in **step 2**. -- `client_id` corresponds to `clientId` -- `client_secret` corresponds to `clientSecret` -- `scope` corresponds to `scope` -- `authorization_endpoint` corresponds to `authorizeUrl` -- `token_endpoint` corresponds to `tokenFetchUrl` +type OAuthTransactionStore interface { + Put(sessionID string, transaction OAuthTransaction) error + Consume(sessionID string) (OAuthTransaction, bool) +} -Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `create_authorization_url`. -This way the user ends up accessing the actual login page served by the **Authorization Service**. +type AppSessionStore interface { + Put(sessionID string, session AppSession) error +} - +type OAuthApp struct { + Config *oauth2.Config + IntrospectionURL string + ExpectedIssuer string + ExpectedAudience string + RequiredScopes []string + Transactions OAuthTransactionStore + Sessions AppSessionStore + ResolveAllowedTenant func(*http.Request) (string, error) + ApplicationSessionID func(*http.Request) string + RotateApplicationSession func(http.ResponseWriter, *http.Request) (string, error) +} - +func randomURLSafeToken(size int) (string, error) { + value := make([]byte, size) + if _, err := rand.Read(value); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(value), nil +} -You can use the [League OAuth2 Client](https://github.com/thephpleague/oauth2-client) library. -Follow these [instructions](https://oauth2-client.thephpleague.com/usage/) and implement it in your `backend`. +func (app *OAuthApp) introspectAndValidateTenant(ctx context.Context, accessToken, expectedTenant string) error { + form := url.Values{ + "token": {accessToken}, + "scope": {strings.Join(app.RequiredScopes, " ")}, + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, app.IntrospectionURL, strings.NewReader(form.Encode())) + if err != nil { + return err + } + request.Header.Set("content-type", "application/x-www-form-urlencoded") -You can determine the configuration parameters based on the response received in **step 2**. -- `clientId` corresponds to `clientId` -- `clientSecret` corresponds to `clientSecret` -- `redirectUri` corresponds to a value from `callbackUrls` -- `urlAuthorize` corresponds to `authorizeUrl` -- `urlAccessToken` corresponds to `tokenFetchUrl` + response, err := http.DefaultClient.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return errors.New("OAuth introspection failed") + } -Make sure that you expose an endpoint that redirects to the authentication URL obtained from calling `getAuthorizationUrl`. -This way the user ends up accessing the actual login page served by the **Authorization Service**. + var tokenInfo IntrospectionResponse + if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&tokenInfo); err != nil { + return err + } + if !tokenInfo.Active || !tokenInfo.Audience.Contains(app.ExpectedAudience) || tokenInfo.TenantID != expectedTenant || tokenInfo.ClientID != app.Config.ClientID || tokenInfo.Issuer != app.ExpectedIssuer { + return errors.New("OAuth token does not match the login transaction") + } + return nil +} - +func (app *OAuthApp) Login(w http.ResponseWriter, r *http.Request) { + state, err := randomURLSafeToken(32) + if err != nil { + http.Error(w, "login unavailable", http.StatusInternalServerError) + return + } + tenantID, err := app.ResolveAllowedTenant(r) + if err != nil { + http.Error(w, "invalid tenant", http.StatusBadRequest) + return + } + verifier := oauth2.GenerateVerifier() + transaction := OAuthTransaction{State: state, Verifier: verifier, TenantID: tenantID} + if err := app.Transactions.Put(app.ApplicationSessionID(r), transaction); err != nil { + http.Error(w, "login unavailable", http.StatusInternalServerError) + return + } - + authURL := app.Config.AuthCodeURL( + state, + oauth2.S256ChallengeOption(verifier), + oauth2.SetAuthURLParam("tenant_id", tenantID), + ) + http.Redirect(w, r, authURL, http.StatusFound) +} -You can use the [Spring Security](https://github.com/spring-projects/spring-security) library. -Follow these [instructions](https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-client-log-users-in) and implement it in your `backend`. +func (app *OAuthApp) Callback(w http.ResponseWriter, r *http.Request) { + transaction, ok := app.Transactions.Consume(app.ApplicationSessionID(r)) + providedState := r.URL.Query().Get("state") + if !ok || subtle.ConstantTimeCompare([]byte(transaction.State), []byte(providedState)) != 1 { + http.Error(w, "invalid OAuth state", http.StatusBadRequest) + return + } -You can determine the configuration parameters based on the response received in **step 2**. + token, err := app.Config.Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(transaction.Verifier)) + if err != nil { + http.Error(w, "code exchange failed", http.StatusBadRequest) + return + } + if err := app.introspectAndValidateTenant(r.Context(), token.AccessToken, transaction.TenantID); err != nil { + http.Error(w, "token validation failed", http.StatusUnauthorized) + return + } -- `client-id` corresponds to `clientId` -- `client-secret` corresponds to `clientSecret` -- `scope` corresponds to `scope` -- `issuer-uri` corresponds to `^{appInfo.apiDomain}^{appInfo.apiBasePath}` + newSessionID, err := app.RotateApplicationSession(w, r) + if err != nil { + http.Error(w, "session creation failed", http.StatusInternalServerError) + return + } + if err := app.Sessions.Put(newSessionID, AppSession{TenantID: transaction.TenantID, Token: token}); err != nil { + http.Error(w, "session creation failed", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/", http.StatusFound) +} +``` - +Create an `OAuthApp` with your OAuth client configuration, server-side stores, and application session helpers, then register its `Login` and `Callback` methods as HTTP handlers. `ResolveAllowedTenant` must reject tenants outside your allowlist. `ApplicationSessionID` must identify the initiating server-side session, and `RotateApplicationSession` must invalidate the old session and issue a new opaque session ID. `Transactions.Consume` must atomically read and delete an unexpired transaction; return `false` for missing or expired transactions. + +Set `IntrospectionURL` to `/auth/oauth/introspect`, `ExpectedIssuer` to the exact configured issuer, `ExpectedAudience` to this resource server, and `RequiredScopes` to its scopes. Core 12.1.1 introspection can return `aud` as a JSON string or array; `Audience.UnmarshalJSON` handles both and the callback requires the configured audience before creating a session. The released `tenant_id` authorization parameter selects the tenant, and introspection returns its signed `tId`. Only the rotated opaque session ID reaches the browser. + + +Use [Authlib](https://docs.authlib.org/) with a one-time server-side transaction store. These functions are framework-agnostic; connect `redirect`, `request_url`, and the session helpers to your framework. + +```python +import secrets + +import requests +from authlib.common.security import generate_token +from authlib.integrations.requests_client import OAuth2Session + + +def introspect_and_validate_tenant(access_token: str, expected_tenant: str) -> None: + response = requests.post( + INTROSPECTION_URL, + data={"token": access_token, "scope": " ".join(REQUIRED_SCOPES)}, + timeout=5, + ) + response.raise_for_status() + token_info = response.json() + audiences = token_info.get("aud", []) + if isinstance(audiences, str): + audiences = [audiences] + if ( + token_info.get("active") is not True + or token_info.get("tId") != expected_tenant + or token_info.get("client_id") != CLIENT_ID + or token_info.get("iss") != EXPECTED_ISSUER + or EXPECTED_AUDIENCE not in audiences + ): + raise ValueError("OAuth token does not match the login transaction") + + +def login(): + client = OAuth2Session( + CLIENT_ID, + CLIENT_SECRET, + token_endpoint_auth_method="client_secret_basic", + scope=SCOPES, + redirect_uri=CALLBACK_URL, + code_challenge_method="S256", + ) + verifier = generate_token(48) + tenant_id = resolve_allowed_tenant() + authorization_url, state = client.create_authorization_url( + AUTHORIZATION_URL, + code_verifier=verifier, + tenant_id=tenant_id, + ) + transaction_store.put( + application_session_id(), + {"state": state, "verifier": verifier, "tenant_id": tenant_id}, + ) + return redirect(authorization_url) + + +def callback(): + # consume atomically reads and deletes the initiating session's transaction + transaction = transaction_store.consume(application_session_id()) + provided_state = request_query_parameter("state") or "" + if transaction is None or not secrets.compare_digest( + transaction["state"], provided_state + ): + raise InvalidOAuthState() + + client = OAuth2Session( + CLIENT_ID, + CLIENT_SECRET, + token_endpoint_auth_method="client_secret_basic", + state=transaction["state"], + redirect_uri=CALLBACK_URL, + code_challenge_method="S256", + ) + token = client.fetch_token( + TOKEN_URL, + authorization_response=request_url(), + code_verifier=transaction["verifier"], + ) + introspect_and_validate_tenant(token["access_token"], transaction["tenant_id"]) + + new_session_id = rotate_application_session() + application_session_store.put( + new_session_id, + {"tenant_id": transaction["tenant_id"], "oauth_token": token}, + ) + return redirect("/") +``` - +Set `INTROSPECTION_URL` to `/auth/oauth/introspect`, `EXPECTED_ISSUER` to the exact configured issuer, and configure the expected client, audience, and required scopes. Authlib sends the released `tenant_id` authorization parameter. Core introspection validates signature, expiry, revocation, and requested scopes and returns the signed `tId`; only the rotated opaque session ID is sent to the browser. + + +Use [League OAuth2 Client](https://oauth2-client.thephpleague.com/usage/) with a server-side application session and one-time transaction store. -You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library. -Follow these [instructions](https://identitymodel.readthedocs.io/en/latest/client/token.html#requesting-a-token-using-the-authorization-code-grant-type) and implement it in your `backend`. +```php +$clientSecret = getenv('OAUTH_CLIENT_SECRET'); +if ($clientSecret === false) { + throw new RuntimeException('OAUTH_CLIENT_SECRET is required'); +} -You can determine the configuration parameters based on the response received in **step 2**. +$httpClient = new GuzzleHttp\Client(['timeout' => 5]); +$provider = new League\OAuth2\Client\Provider\GenericProvider( + [ + 'clientId' => CLIENT_ID, + 'clientSecret' => $clientSecret, + 'redirectUri' => 'https:///oauth/callback', + 'urlAuthorize' => '/auth/oauth/auth', + 'urlAccessToken' => '/auth/oauth/token', + 'urlResourceOwnerDetails' => '/auth/oauth/userinfo', + 'scopes' => ['offline_access', '', ''], + 'scopeSeparator' => ' ', + 'pkceMethod' => League\OAuth2\Client\Provider\GenericProvider::PKCE_METHOD_S256, + ], + [ + 'httpClient' => $httpClient, + 'optionProvider' => new League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider(), + ], +); + +if ($requestPath === '/login') { + $tenantId = resolveAllowedTenant(); + $authorizationUrl = $provider->getAuthorizationUrl([ + 'tenant_id' => $tenantId, + ]); + $transactionStore->put(session_id(), [ + 'state' => $provider->getState(), + 'pkceCode' => $provider->getPkceCode(), + 'tenantId' => $tenantId, + ]); + header('Location: ' . $authorizationUrl); + exit; +} + +if ($requestPath !== '/oauth/callback') { + throw new RuntimeException('Not found'); +} + +// consume atomically reads and deletes the initiating session's transaction +$transaction = $transactionStore->consume(session_id()); +$providedState = $_GET['state'] ?? ''; +if ($transaction === null || !hash_equals($transaction['state'], $providedState)) { + throw new RuntimeException('Invalid OAuth state'); +} +if (isset($_GET['error']) || !isset($_GET['code'])) { + throw new RuntimeException('OAuth authorization failed'); +} + +$provider->setPkceCode($transaction['pkceCode']); +$token = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'], +]); + +$introspectionResponse = $httpClient->request('POST', INTROSPECTION_URL, [ + 'form_params' => [ + 'token' => $token->getToken(), + 'scope' => implode(' ', REQUIRED_SCOPES), + ], +]); +$tokenInfo = json_decode( + (string) $introspectionResponse->getBody(), + true, + 512, + JSON_THROW_ON_ERROR, +); +$audiences = is_array($tokenInfo['aud'] ?? null) + ? $tokenInfo['aud'] + : [$tokenInfo['aud'] ?? null]; +if ( + ($tokenInfo['active'] ?? false) !== true + || ($tokenInfo['tId'] ?? null) !== $transaction['tenantId'] + || ($tokenInfo['client_id'] ?? null) !== CLIENT_ID + || ($tokenInfo['iss'] ?? null) !== EXPECTED_ISSUER + || !in_array(EXPECTED_AUDIENCE, $audiences, true) +) { + throw new RuntimeException('OAuth token does not match the login transaction'); +} -- `Address` corresponds to `^{appInfo.apiDomain}^{appInfo.apiBasePath}` -- `ClientId` corresponds to `clientId` -- `ClientSecret` corresponds to `clientSecret` -- `RedirectUri` corresponds to a value from `callbackUrls` +session_regenerate_id(true); +$applicationSessionStore->put(session_id(), [ + 'tenantId' => $transaction['tenantId'], + 'oauthToken' => $token, +]); +header('Location: /'); +exit; +``` -Make sure that you expose an endpoint that redirects to the authentication URL obtained by using [this example](https://identitymodel.readthedocs.io/en/latest/misc/request_url.html#authorization-endpoint). -This way the user ends up accessing the actual login page served by the **Authorization Service**. +Set `INTROSPECTION_URL` to `/auth/oauth/introspect`, `EXPECTED_ISSUER` to the exact configured issuer, and configure the expected audience and required scopes. League sends the released `tenant_id` authorization parameter. Core introspection validates signature, expiry, revocation, and requested scopes and returns signed `tId`; only the rotated opaque session ID reaches the browser. + + +You can use the [Spring Security](https://github.com/spring-projects/spring-security) library. +Follow these [instructions](https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html#oauth2-client-log-users-in) and implement it in your `backend`. +You can determine the configuration parameters based on the response received in **step 2**. +- `client-id` corresponds to `clientId` +- `client-secret` corresponds to `clientSecret` +- `scope` corresponds to `scope` +- `issuer-uri` corresponds to `/auth` - +Use an `OAuth2AuthorizationRequestResolver` to add the allowlisted tenant as the `tenant_id` authorization parameter and retain it in the server-side `AuthorizationRequestRepository` transaction. After callback, call `/auth/oauth/introspect`, require `active`, the configured scopes/client/audience/issuer, and exact `tId`, then persist that tenant and the tokens in the rotated server-side application session. + + +Use ASP.NET Core's OpenID Connect authentication middleware to handle the authorization callback, correlation cookie, `state`, `nonce`, token validation, and application-session rotation. Configure its authority as `/auth`, set `ClientId` and `ClientSecret` from the confidential client, and set `CallbackPath` to the path of an exact `redirectUris` entry. Store tokens server-side rather than in the authentication cookie. - +In `OnRedirectToIdentityProvider`, add the allowlisted tenant to `AuthenticationProperties.Items` and send it as `ProtocolMessage.SetParameter("tenant_id", tenantId)`. After callback, introspect the access token at `/auth/oauth/introspect`; require `active`, the configured scopes/client/audience/issuer, and exact `tId`. Put that tenant and the tokens in the rotated server-side application session, never in the browser cookie. + + :::info -If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. +If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: @@ -495,8 +1352,7 @@ In your `frontend` applications you need to add a login action that directs the The user should first redirect to the `backend` authentication endpoint defined during the previous step. There the `backend` generates a safe `authorization` URL using the **OAuth2** library and then redirects the user there. -After the user has logged in from the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) they redirect to the `backend` callback URL. -Then the `backend` creates the authentication session and sends it to the user agent as a cookie. +After login, the Authorization Service redirects the user to the backend callback. The backend verifies the bound `state` (and OIDC `nonce` when applicable), exchanges the code, rotates the application session, and sets only the hardened opaque session cookie described above. ### 7. Test the new authentication flow diff --git a/docs/authentication/unified-login/quickstart-guides/reuse-website-login.mdx b/docs/authentication/unified-login/quickstart-guides/reuse-website-login.mdx index cd1f7ca09f..b5973d88bb 100644 --- a/docs/authentication/unified-login/quickstart-guides/reuse-website-login.mdx +++ b/docs/authentication/unified-login/quickstart-guides/reuse-website-login.mdx @@ -1,19 +1,10 @@ --- title: Reuse website login for desktop and mobile apps -hide_title: true -sidebar_position: 3 -description: Implement web authentication for desktop and mobile apps using OAuth2 -page_type: tutorial -recipe: oauth2 -category: unified-login +description: Implement web authentication for desktop and mobile apps using OAuth 2.0. +sidebar: + order: 3 --- - -import CreateOAuth2ClientRequest from "../_blocks/create-oauth2-client-request-no-auth.mdx"; - -# Reuse website login for desktop and mobile apps - - ## Overview This pattern is useful if you want to have the same web authentication experience for your desktop and mobile apps. @@ -24,27 +15,26 @@ The authentication flow works in the following way: -## User accesses the native application - - The user gets redirected to [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) authentication URL. - - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) redirects the user to the login UI - -## User completes the login attempt - - The [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server) backend redirects the user to the `callback URL`. - - This URL should be a deep link that your actual application can open. - -## The application uses the callback URL information to request a **OAuth2 Access Token** from the [**Authorization Service**](/docs/authentication/unified-login/oauth2-basics#authorization-server). - - The application saves the returned token. + +- The user gets redirected to [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) authentication URL. + - The [**Authorization Service**](/authentication/unified-login/oauth2-basics#authorization-server) redirects the user to the login UI + + +- The Authorization Service redirects the user to the registered callback URL. + - Prefer an OS-claimed HTTPS universal link or app link. If you must use a custom scheme, follow the platform guidance for preventing other apps from claiming it. + + +- The application verifies `state`, then exchanges the code with the original PKCE verifier. +- It stores returned tokens only in platform-protected secure storage. + -Reuse website login for desktop and mobile apps +Reuse website login for desktop and mobile apps ## Before you start - - -These instructions assume that you already have gone through the main [quickstart guide](/docs/quickstart/introduction). -If you have skipped that page, please follow the tutorial and return here once you're done. + ## Steps @@ -56,89 +46,285 @@ Go to the [**SuperTokens.com SaaS Dashboard**](https://supertokens.com/dashboard ### 2. Create the OAuth2 Clients -For each of your applications you need to create a separate [**OAuth2 client**](/docs/authentication/unified-login/oauth2-basics#client). -You can do this by directly calling the **SuperTokens Core** API. - - - - -:::caution -You have to save the create OAuth2 Client response because this is not persisted internally for security reasons. -The information is necessary for the next steps. +For each native application, create a separate [**OAuth2 client**](/authentication/unified-login/oauth2-basics#client). +Call the **SuperTokens Core** API from a trusted administrative environment. Native applications are **public clients**: `tokenEndpointAuthMethod` must be `none`, and the application must never contain or receive a client secret. Use authorization code with S256 PKCE. + + + + + + +```bash +curl --location --request POST '/recipe/oauth/clients' \ + --header 'api-key: ' \ + --header 'Content-Type: application/json; charset=utf-8' \ + --data ' + { + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "scope": "offline_access ", + "redirectUris": ["https://app.example.com/oauth/callback"] + } +' +``` + + +```tsx +const BASE_URL = ""; +const API_KEY = ""; + +const url = `${BASE_URL}/recipe/oauth/clients`; +const options = { + method: "POST", + headers: { + "api-key": API_KEY, + "Content-Type": "application/json; charset=utf-8", + }, + body: JSON.stringify({ + clientName: "", + responseTypes: ["code"], + grantTypes: ["authorization_code", "refresh_token"], + tokenEndpointAuthMethod: "none", + scope: "offline_access ", + redirectUris: ["https://app.example.com/oauth/callback"], + }), +}; + +fetch(url, options) + .then((response) => response.json()) + .then((json) => console.log(json)) + .catch((err) => console.error(err)); +``` + + +```go + +import ( + "fmt" + "net/http" + "strings" + "io" +) + +func main() { + baseUrl := "" + apiKey := "" + url := fmt.Sprintf("%s/recipe/oauth/clients", baseUrl) + payload := `{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "scope": "offline_access ", + "redirectUris": ["https://app.example.com/oauth/callback"] + }` + + req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) + + req.Header.Add("accept", "application/json") + req.Header.Add("api-key", apiKey) + req.Header.Add("content-type", "application/json") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := io.ReadAll(res.Body) + + fmt.Println(string(body)) +} +``` + + +```python +import requests +from typing import Dict, Any + +BASE_URL = "" +API_KEY = "" + +url = f"{BASE_URL}/recipe/oauth/clients" + +payload: Dict[str, Any] ={ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "scope": "offline_access ", + "redirectUris": ["https://app.example.com/oauth/callback"] +} + +headers = { + "api-key": API_KEY, + "Content-Type": "application/json", +} + +response = requests.post(url, json=payload, headers=headers) + +print(response.json()) +``` + + + + +Creates an OAuth2 client +**Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. +## Request +### Body Schema +| Name | Type | Description | Required | Default Value | +|--------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------| +| `clientName` | `string` | A human-readable name of the client used for identification. | Yes | - | +| `grantTypes` | `array` of `GrantType` | The grant types that the Client uses. | Yes | - | +| `redirectUris` | `array` of `string` | Exact redirect URIs registered for the client. Wildcards are not supported. | Yes | - | +| `scope` | `string` | String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. Include the `offline_access` scope to exchange OAuth2 Refresh Tokens for OAuth2 Access Tokens | No | "" | +| `responseTypes` | `array` of `ResponseType` | The types of responses your client expects from the **Authorization Server** | No | - | +| `tokenEndpointAuthMethod` | `enum`(`"client_secret_basic"`, `"client_secret_post"`, `"private_key_jwt"`, `"none"`) | The requested client authentication method | No | `client_secret_basic` | +| `authorizationCodeGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantIdTokenLifespan` | `Time Duration` | OAuth2 ID Token lifespan when using the Authorization Code grant flow. | No | `"1h"` | +| `authorizationCodeGrantRefreshTokenLifespan`| `Time Duration` | OAuth2 Refresh Token lifespan when using the Authorization Code grant flow. | If `refreshTokenGrantRefreshTokenLifespan` is also set | `"30d"` | +| `refreshTokenGrantRefreshTokenLifespan` | `Time Duration` | OAuth2 Refresh Token lifespan when using the Refresh Token grant flow. Must match `authorizationCodeGrantRefreshTokenLifespan`. | If `authorizationCodeGrantRefreshTokenLifespan` is also set | `"30d"` | +| `clientCredentialsGrantAccessTokenLifespan` | `Time Duration` | OAuth2 Access Token lifespan when using the Client Credentials grant flow. | No | `"1h"` | +| `enableRefreshTokenRotation` | `boolean` | Indicates that the refresh token is a one-time use. Set it to `false` to disable refresh token rotation. | No | `true` | +#### GrantType +- `authorization_code`: allows exchanging the Authorization Code for an OAuth2 Access Token. +- `refresh_token`: allows exchanging the OAuth2 Refresh Token for an OAuth2 Access Token. +- `client_credentials`: allows the client to directly request an OAuth2 Access Token by authenticating itself with the Authorization Server using its own client credentials. +#### TokenEndpointAuthMethod +- `client_secret_basic`: uses the HTTP Basic Authentication scheme to authenticate the client. +- `client_secret_post`: uses the HTTP `POST` Authentication scheme to authenticate the client. +- `private_key_jwt`: uses JSON Web Tokens (JWT) to authenticate the client. +- `none`: indicates that the process of obtaining an OAuth2 Access Token does not use the client secret. Used for public clients (native apps or mobile apps). +#### ResponseType +- `code`: Indicates that the Client receives an Authorization Code that it exchanges for an OAuth2 Access Token. +- `id_token`: Indicates that the Client expects an ID Token. +#### Time Duration +A string value that signifies time duration in milliseconds, seconds, minutes, or hours: `"2000ms"`, `"60s"`, `"30m"`, `"1h"`. +### Example +```bash +curl -X POST /recipe/oauth/clients \ + -H "Content-Type: application/json" \ + -H "api-key: " \ + -d '{ + "clientName": "", + "responseTypes": ["code"], + "grantTypes": ["authorization_code", "refresh_token"], + "tokenEndpointAuthMethod": "none", + "scope": "offline_access ", + "redirectUris": ["https://app.example.com/oauth/callback"] + }' +``` +## Response +### 200 +The client has been successfully created. +### Relevant response fields + +The response includes the persisted client configuration, including the fields below. + +| Property | Type | Description | +|-------------|----------------------------------|-----------------------------------------------| +| `clientName` | `string` | The name of the client. | +| `clientId` | `string` | Unique identifier for the client. | +| `clientSecret` | `string` | Client secret for a confidential client. Omitted for a public client. Treat it as a credential and keep it on a trusted backend. | +| `redirectUris` | `array` of `string` | The URLs used for redirection. | +| `audience` | `array` of `string` | Value used to identify for whom a token is issued. The created client can generate access token only for the specified audiences. | +| `scope` | `string` | A space-separated string of scopes that the client can request. | +| `responseTypes` | `array` of `string` | Registered response types. | +| `grantTypes` | `array` of `string` | Registered grant types. | +| `tokenEndpointAuthMethod` | `string` | Token endpoint authentication method. | +| `enableRefreshTokenRotation` | `boolean` | Whether refresh token rotation is enabled. | +#### Example +```json +{ + "clientName": "", + "clientId": "", + "tokenEndpointAuthMethod": "none", + "redirectUris": ["https://app.example.com/oauth/callback"], + "audience": [""], + "scope": "offline_access " +} +``` + + + +:::warning[Protect OAuth client credentials] +Core persists the client configuration and encrypts confidential client secrets at rest. Store any returned client secret in a secret manager and expose it only to the application backend. Public clients do not receive or use a client secret. ::: Based on the client creation process, you can infer two additional values that you need later on: -- `authorizeUrl` corresponds to `^{appInfo.apiDomain}^{appInfo.apiBasePath}/oauth/auth` -- `tokenFetchUrl` corresponds to `^{appInfo.apiDomain}^{appInfo.apiBasePath}/oauth/token` +- `authorizeUrl` corresponds to `/auth/oauth/auth` +- `tokenFetchUrl` corresponds to `/auth/oauth/token` ### 3. Configure the Authorization Service Check one of the previous guides that show you how to set up the **Authorization Service** and then return to this page. -Choose the tutorial based whether you use multiple backend services or not: -- [Single Backend Setup](/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) -- [Multiple Backends Setup](/docs/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) +Choose the tutorial based on whether you use multiple backend services or not: +- [Single Backend Setup](/authentication/unified-login/quickstart-guides/multiple-frontends-with-a-single-backend#3-set-up-the-authorization-service-backend) +- [Multiple Backends Setup](/authentication/unified-login/quickstart-guides/multiple-frontends-with-separate-backends#3-set-up-your-authorization-service-backend) ### 4. Update the login flow in your applications In each of your individual `applications`, you need to set up logic for handling the **OAuth 2.0** authentication flow. -You can use a generic **OIDC** or **OAuth** library to do this. +Use a maintained native OAuth 2.0/OIDC library that uses the system browser and authorization code with S256 PKCE. For every request, let the library generate a fresh PKCE verifier and high-entropy `state`; verify `state` before code exchange. If you request `openid`, also generate and validate `nonce` and validate the ID token's signature, issuer, audience, expiry, and nonce. Never use an embedded web view. - +Register an exact callback URI. Prefer an OS-claimed HTTPS universal link or app link; use a custom scheme only when the platform's interception protections are configured. Store access and refresh tokens in Keychain, Android Keystore-backed storage, or the platform equivalent. Never put tokens in logs, URLs, plain-text preferences, or application bundles. - + - + -You can use the [react-native-app-auth](https://commerce.nearform.com/open-source/react-native-app-auth/) library. -Follow [the instructions](https://commerce.nearform.com/open-source/react-native-app-auth/docs/usage/config) to set up your application. -You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. -- `issuer` corresponds to the endpoint of the **Authorization Service** `^{appInfo.apiDomain}^{appInfo.apiBasePath}` -- `clientID` corresponds to `clientId` -- `redirectUrl` corresponds a value from `callbackUrls` -- `scopes` corresponds to `scopes` +You can use the [react-native-app-auth](https://commerce.nearform.com/open-source/react-native-app-auth/) library. +Follow [the instructions](https://commerce.nearform.com/open-source/react-native-app-auth/docs/usage/config) to set up your application. + +Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. + +- `issuer` corresponds to the endpoint of the **Authorization Service** `/auth` +- `clientId` corresponds to `clientId` +- `redirectUrl` corresponds to a value from `redirectUris` +- `scopes` is the space-separated `scope` value split into a list You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. - - - + + + - -You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library. -Follow [the instructions](https://github.com/openid/AppAuth-Android?tab=readme-ov-file#authorization-service-configuration) to set up your application. -You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. +You can use the [AppAuth-Android](https://github.com/openid/AppAuth-Android) library. +Follow [the instructions](https://github.com/openid/AppAuth-Android?tab=readme-ov-file#authorization-service-configuration) to set up your application. + +Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. For the `AuthorizationServiceConfiguration`, the parameters you need to provide are: `authorizeUrl` and `tokenFetchUrl`. -When calling the `AuthorizationRequest.Builder` function you can use `clientId` and a value from `callbackUrls` to replace the example values. +When calling the `AuthorizationRequest.Builder` function you can use `clientId` and a value from `redirectUris` to replace the example values. You need to set additional query parameters by calling the `setAdditionalParameters` function on the `AuthorizationRequest.Builder` object: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. - - - + + + - -You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-Android) library. -Follow [the instructions](https://github.com/openid/AppAuth-iOS?tab=readme-ov-file#auth-flow) to set up your application. -You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. +You can use the [AppAuth-iOS](https://github.com/openid/AppAuth-iOS) library. +Follow [the instructions](https://github.com/openid/AppAuth-iOS?tab=readme-ov-file#auth-flow) to set up your application. -- `clientID` corresponds to `clientId` -- `redirectUrl` corresponds a value from `callbackUrls` -- `scopes` corresponds to `scopes` +Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. + +- `clientId` corresponds to `clientId` +- `redirectUrl` corresponds to a value from `redirectUris` +- `scopes` is the space-separated `scope` value split into a list - `authorizationEndpoint` corresponds to `authorizeUrl` - `tokenEndpoint` corresponds to `tokenFetchUrl` @@ -146,37 +332,38 @@ You also need to set extra query parameters, when instantiating the `OIDAuthoriz - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. - - - + + + + - -You can use the [AppAuth](https://github.com/MaikuB/flutter_appauth) library. -Follow [the instructions](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth) to set up your application. +You can use the [AppAuth](https://github.com/MaikuB/flutter_appauth) library. +Follow [the instructions](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth) to set up your application. -You can identify the configuration parameters based on the response received in **step 2**, when creating the **OAuth2 Client**. +Use authorization code with S256 PKCE and validate `state`. You can identify the configuration parameters from the response in **step 2**. - `` corresponds to `clientId` -- `` corresponds to the endpoint of the **Authorization Service** `^{appInfo.apiDomain}^{appInfo.apiBasePath}` -- `` corresponds a value from `callbackUrls` -- `scopes` corresponds to `scopes` +- `` corresponds to the endpoint of the **Authorization Service** `/auth` +- `` corresponds to a value from `redirectUris` +- `scopes` is the space-separated `scope` value split into a list You also need to set the `additionalParameters` property with the following values: - `max_age: 0` This forces a new authentication flow once the user ends up on the **Authorization Service** frontend. - `tenant_id: ` Optional, in case you are using a multi tenant setup. Set this to the actual tenant ID. - - + + + - + :::info -If you want to use the [**OAuth2 Refresh Tokens**](/docs/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. +If you want to use the [**OAuth2 Refresh Tokens**](/authentication/unified-login/oauth2-basics#oauth2-refresh-token) make sure to include the `offline_access` scope during the initialization step. ::: diff --git a/docs/authentication/unified-login/verify-tokens.mdx b/docs/authentication/unified-login/verify-tokens.mdx index 9b228ed5f7..17de06e64b 100644 --- a/docs/authentication/unified-login/verify-tokens.mdx +++ b/docs/authentication/unified-login/verify-tokens.mdx @@ -1,363 +1,79 @@ --- title: Verify tokens -hide_title: true -sidebar_position: 5 -toc_max_heading_level: 4 -description: 'Verify OAuth2 tokens using JWT libraries or the introspection endpoint ' -page_type: guide -recipe: oauth2 -category: unified-login +description: Verify OAuth2 access tokens locally or with database-backed validation. +sidebar: + order: 5 --- +## Overview -# Verify tokens - - -## Overview - -You can verify an **OAuth2 Access Token** in two ways: -- By using a standard **JWT**, JSON Web Token, verification library -- By calling the special introspection endpoint that gets exposed through the core service +You can verify an **OAuth2 Access Token** locally or enable database-backed validation to detect revocation. One thing to note is that, besides the standard **OAuth2** token claims, the **Unified Login** implementation includes an additional one called `stt`. This stands for `SuperTokens Token Type`. It ensures that the validation occurs for the correct token type: -- `0` represents a **SuperTokens Session Access Token** +- `0` represents a **SuperTokens Session Access Token** - `1` represents an **OAuth2 Access Token** - `2` represents an **OAuth2 ID Token**. -:::caution no-title +:::warning The following guide covers only **OAuth2 Tokens** verification. -For information on how to verify **SuperTokens Session Tokens** please refer to the [following section](/docs/additional-verification/session-verification/protect-api-routes). +For information on how to verify **SuperTokens Session Tokens** please refer to the [following section](/additional-verification/session-verification/protect-api-routes). ::: --- -## Using a JWT verification library - -This is the standard validation method and you should use it for most of the operations that need protection. -It indicates that the token is valid from a cryptographic standpoint and that it has not expired. - -The code samples show you a basic validation scenario where the token undergoes a check for the required scope for an action to occur. - -:::info - -If your scenario involves a common backend with multiple frontend clients you can drop the `client_id` check. +## Local access token verification -::: - - - - - - - -For NodeJS you can use [`jose`](https://github.com/panva/jose) to verify the token. +Use the released SuperTokens backend SDK validator for most protected operations. It validates the JWT signature, +expiration, and `stt=1` token type. Configure the intended audience and required scopes. Restricting the client ID is an +optional additional check; it does not replace audience validation. Also compare the token issuer with your Authorization +Server's issuer. + + ```tsx -import * as jose from "jose"; - -const JWKS = jose.createRemoteJWKSet(new URL('^{appInfo.apiDomain}^{appInfo.apiBasePath}jwt/jwks.json')) - -// Follow this example if you are using the Authorization Code Flow -async function validateToken(jwt: string) { - const requiredScope = ""; - const clientId = ''; +import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; +async function validateToken(token: string): Promise { try { - const { payload } = await jose.jwtVerify(jwt, JWKS, { - requiredClaims: ['stt', 'scp', 'client_id'], + const result = await OAuth2Provider.validateOAuth2AccessToken(token, { + audience: "", + clientId: "", + scopes: [""], }); - if(payload.stt !== 1) return false; - if(payload.client_id !== clientId) return false; - - const scopes = payload.scp as string[]; - return scopes.includes(requiredScope); - } catch (err) { + return result.payload.iss === "/auth"; + } catch { return false; } } ``` + + +```python +from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements +from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token - - - - -You can use the [`jwx`](https://github.com/lestrrat-go/jwx) library to verify the token. - -```go -import ( - "context" - "fmt" - - "github.com/lestrrat-go/jwx/jwt" - "github.com/lestrrat-go/jwx/jwk" -) - -func ValidateToken(token string) bool { - apiDomain := "^{appInfo.apiDomain}" - apiBasePath := "^{appInfo.apiBasePath}" - clientId := "" - requiredScope := "" - - jwksURL := fmt.Sprintf("%s%sjwt/jwks.json", apiDomain, apiBasePath) - jwks, err := jwk.Fetch(context.Background(), jwksURL) - if err != nil { - return false - } - - parsedToken, err := jwt.Parse( - []byte(token), - jwt.WithKeySet(jwks), - jwt.WithClaimValue("stt", 1), - jwt.WithClaimValue("client_id", clientId), - ) - if err != nil { - return false - } - - scp, ok := parsedToken.Get("scp") - if !ok { - return false - } - - scopes, ok := scp.([]interface{}) - if !ok { - return false - } - - for _, scope := range scopes { - if scope, ok := scope.(string); ok && scope == requiredScope { - return true - } - } - - return false; -} -``` - - - - - -You can use the [PyJWT](https://github.com/jpadilla/pyjwt) library to verify the token. - -```python -from typing import Optional, List -import jwt -from jwt import PyJWKClient def validate_token(token: str) -> bool: - api_domain = "^{appInfo.apiDomain}" - api_base_path = "^{appInfo.apiBasePath}" - client_id = "" - required_scope = "" - - jwks_url = f"{api_domain}{api_base_path}jwt/jwks.json" - jwks_client = PyJWKClient(jwks_url) - - try: - signing_key = jwks_client.get_signing_key_from_jwt(token) - decoded = jwt.decode( - token, - signing_key.key, - algorithms=['RS256'], - options={"require": ["stt", "client_id", "scp"]} - ) - - stt: Optional[int] = decoded.get('stt') - if stt != 1: - return False - - token_client_id: Optional[str] = decoded.get('client_id', None) - if client_id != token_client_id: - return False - - - scopes: List[str] = decoded.get('scp', []) - if required_scope not in scopes: - return False - - return True - except Exception: - return False + try: + result = validate_oauth2_access_token( + token=token, + requirements=OAuth2TokenValidationRequirements( + audience="", + client_id="", + scopes=[""], + ), + ) + return result.payload.get("iss") == "/auth" + except Exception: + return False ``` - - - - - -You can use the [Firebase JWT](https://github.com/firebase/php-jwt) library to verify the token. - -```php - -require 'vendor/autoload.php'; - -use Firebase\JWT\JWT; -use Firebase\JWT\Key; - -function validateToken($jwt) { - $apiDomain = "^{appInfo.apiDomain}"; - $apiBasePath = "^{appInfo.apiBasePath}"; - $jwksUrl = $apiDomain . $apiBasePath . '/jwt/jwks.json'; - $requiredScope = ""; - $clientId = ""; - - $jwks = json_decode(file_get_contents($jwksUrl), true); - try { - $decoded = JWT::decode($jwt, JWK::parseKeySet($jwks), 'RS256')); - if ($decoded->sst !== 1) { - return false; - } - if ($decoded->client_id !== $clientId) { - return false; - } - return in_array($requiredScope, $decoded->scp); - } catch (Exception $e) { - return false; - } -} -``` - - - - - -You can use the [Auth0 JWT](https://github.com/auth0/java-jwt) library to verify the token. -Here is an example of how you can do that: - -```java - -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.auth0.jwt.interfaces.JWTVerifier; -import com.auth0.jwt.JWTVerifier.Base; -import com.auth0.jwt.algorithms.Algorithm; - -import java.net.URL; -import java.util.Map; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; - -public class JWTVerifier { - - private static final String JWKS_URL = "^{appInfo.apiDomain}^{appInfo.apiBasePath}jwt/jwks.json"; - private static final String CLIENT_ID = ""; - - private static Map fetchJWKS() throws Exception { - URL url = new URL(JWKS_URL); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - InputStream responseStream = connection.getInputStream(); - Scanner scanner = new Scanner(responseStream, StandardCharsets.UTF_8.name()); - String responseBody = scanner.useDelimiter("\\A").next(); - scanner.close(); - - return JWT.decode(responseBody).getHeader(); - } - - public static boolean validateToken(String token) { - try { - Map jwks = fetchJWKS(); - - Algorithm algorithm = Algorithm.RSA256(jwks.get("x5c"), null); - JWTVerifier verifier = JWT.require(algorithm) - .build(); - - DecodedJWT jwt = verifier.verify(token); - if(jwt.getClaim("sst").asInt() != 1) { - return false; - } - - if(jwt.getClaim("client_id").asString() != CLIENT_ID) { - return false; - } - - List scopes = jwt.getClaim("scp").asList(); - return scopes.contains(requiredScope); - } catch (Exception e) { - return false; - } - } -} -``` - - - - - -You can use the [IdentityModel](https://github.com/IdentityModel/IdentityModel) library to verify the token. - -```csharp - -using System; -using System.Linq; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; - -class AuthorizationCodeTokenValidator -{ - static async Task ValidateToken(string jwtStr) - { - string apiDomain = ""; - string apiBasePath = ""; - string clientId = ""; - string requiredScope = ""; - - HttpClient client = new HttpClient(); - var response = await client.GetStringAsync($"//jwt/jwks.json"); - var jwks = new JsonWebKeySet(response); - - var tokenHandler = new JwtSecurityTokenHandler(); - var validationParameters = new TokenValidationParameters - { - IssuerSigningKeys = jwks.Keys - }; - - try - { - SecurityToken validatedToken; - var principal = tokenHandler.ValidateToken(jwtStr, validationParameters, out validatedToken); - var claims = principal.Claims.ToDictionary(c => c.Type, c => c.Value); - - if (!claims.ContainsKey("stt") || claims["stt"] != "1") - { - return false; - } - - if (!claims.ContainsKey("client_id") || claims["client_id"] != clientId) - { - return false; - } - - var scopes = claims["scp"].Split(" "); - if (!scopes.Contains(requiredScope)) - { - return false; - } - - return true; - } - catch (Exception) - { - return false; - } - } -} -``` - - - - + + ### Email verification @@ -367,84 +83,57 @@ If you are using email and password based authentication, and you want to valida ## Using the token introspection API -When a user logs out, their token gets removed from the **SuperTokens Core** database. -That change does not reflect in token validation process that uses a JWT verification library. -The token remains valid until its expiration time. - -To ensure that the token remains valid, you can directly call the **SuperTokens Core** service. -It is advisable to perform this process for high security operations to avoid the risk of a malicious agent using a token. +Revocation is not visible to local JWT verification, so a revoked token otherwise remains valid until it expires. +For high-security operations, use the backend SDK validator with database checking enabled. This calls Core introspection in +addition to performing local cryptographic and claim validation. Here is an example of how you can use this validation method: - - - - + + ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; -async function validateToken(token: string) { - const { status } = await OAuth2Provider.validateOAuth2AccessToken( - token, - { - clientId: "", - scopes: [""], - }, - true - ); - - return status === "OK"; +async function validateToken(token: string): Promise { + try { + const result = await OAuth2Provider.validateOAuth2AccessToken( + token, + { + audience: "", + clientId: "", + scopes: [""], + }, + true, + ); + + return result.payload.iss === "/auth"; + } catch { + return false; + } } ``` - - - - - -:::caution - -The Go SDK does not support creating OAuth2 providers. - -::: - - - - - + + ```python from supertokens_python.recipe.oauth2provider.interfaces import OAuth2TokenValidationRequirements -import supertokens_python.recipe.oauth2provider.syncio as OAuth2Provider - -def validate_token(token: str): - requirements = OAuth2TokenValidationRequirements( - "", - [""] - ) - OAuth2Provider.validate_oauth2_access_token( - token=token, - requirements=requirements, - check_database=True, - ) +from supertokens_python.recipe.oauth2provider.syncio import validate_oauth2_access_token +def validate_token(token: str) -> bool: + try: + result = validate_oauth2_access_token( + token=token, + requirements=OAuth2TokenValidationRequirements( + audience="", + client_id="", + scopes=[""], + ), + check_database=True, + ) + return result.payload.get("iss") == "/auth" + except Exception: + return False ``` - - - - - -```bash -# The response status should be 200 -# and the body should have an "active" property set to true -curl -X POST \ - -H "Content-Type: application/json" \ - -d '{"token": ""}' \ - "^{appInfo.apiDomain}^{appInfo.apiBasePath}/oauth/introspect" -``` - - - - - - + + diff --git a/docs/authentication/unified-login/work-with-scopes.mdx b/docs/authentication/unified-login/work-with-scopes.mdx index 66ecf22527..333b7b51f1 100644 --- a/docs/authentication/unified-login/work-with-scopes.mdx +++ b/docs/authentication/unified-login/work-with-scopes.mdx @@ -1,18 +1,10 @@ --- title: Work with scopes -hide_title: true -sidebar_position: 4 -description: >- - Manage OAuth2 scopes, request specific scopes, and override granted scopes - during authentication. -page_type: guide -recipe: oauth2 -category: unified-login +description: Manage OAuth2 scopes, request specific scopes, and override granted scopes during authentication. +sidebar: + order: 4 --- - -# Work with scopes - ## Overview The creation process of an **OAuth2 Client** determines the allowed scopes. @@ -24,7 +16,7 @@ By default, the **OAuth2** implementation adds the following built-in scopes: | `phoneNumber` | `phoneNumber`, `phoneNumbers`, `phoneNumber_verified` | Added to ID Token and User Info | | `roles` | The roles return by `getRolesForUser` | Added to ID Token and Access Token | | `permissions` | The list of permissions obtained by concatenating the result of `getPermissionsForRole` for all roles returned by `getRolesForUser` | Added to ID Token and Access Token | - + --- ## Request specific scopes @@ -39,45 +31,42 @@ By default, the client receives all scopes. If you want to manually modify the list of scopes that the client receives during the authentication flow, you can do this by using overrides. - - - + + +:::warning[The Go SDK does not support creating OAuth2 providers.] +::: + + + + ```tsx import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; OAuth2Provider.init({ - override: { - functions: (originalFunctions) => ({ - ...originalFunctions, - getRequestedScopes: async (input) => { - const originallyRequestedScopes = await originalFunctions.getRequestedScopes(input); - const filteredScopes = originallyRequestedScopes.filter((scope) => scope !== "profile"); - return [...filteredScopes, "custom-scope"]; - }, - }), - }, + override: { + functions: (originalFunctions) => ({ + ...originalFunctions, + getRequestedScopes: async (input) => { + const originallyRequestedScopes = await originalFunctions.getRequestedScopes(input); + const filteredScopes = originallyRequestedScopes.filter((scope) => scope !== "profile"); + return [...filteredScopes, "custom-scope"]; + }, + }), + }, }); ``` + + - - - - -:::caution -The Go SDK does not support creating OAuth2 providers. -::: - - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import oauth2provider +from supertokens_python.recipe import oauth2provider from supertokens_python.recipe.oauth2provider.interfaces import RecipeInterface from supertokens_python.types import RecipeUserId -from typing import Dict, List, Any, Optional +from typing import Dict, List, Any, Optional def override_oauth2provider_functions(original_implementation: RecipeInterface): original_get_requested_scopes = original_implementation.get_requested_scopes @@ -119,7 +108,5 @@ init( ``` - - - - + + diff --git a/docs/deployment/_category_.json b/docs/deployment/_category_.json deleted file mode 100644 index 81a4840be8..0000000000 --- a/docs/deployment/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Deployment", - "collapsible": true, - "collapsed": true, - "customProps": { - "isMainCategory": true - }, - "position": 8 -} diff --git a/docs/deployment/meta.ts b/docs/deployment/meta.ts new file mode 100644 index 0000000000..04b24c1ee8 --- /dev/null +++ b/docs/deployment/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Deployment", + icon: "server", + order: 90, + collapsed: true, +}); diff --git a/docs/deployment/migrate-from-mysql.mdx b/docs/deployment/migrate-from-mysql.mdx index 1c2f09e080..10c4ea6791 100644 --- a/docs/deployment/migrate-from-mysql.mdx +++ b/docs/deployment/migrate-from-mysql.mdx @@ -1,17 +1,30 @@ -# Migrate from MySQL to PostgreSQL +--- +title: Migrate from MySQL to PostgreSQL +--- -## Overview - -This tutorial shows you how to migrate your **SuperTokens** database from **MySQL** to **PostgreSQL**. -The migration involves exporting data from MySQL, setting up a PostgreSQL database with the same schema version, and importing the data with proper format conversions. +This is a legacy migration path. The released schemas verified for this page are the matching MySQL and PostgreSQL +Core `10.1.4` images, which use storage interface `7.1`. Current Core releases use a newer storage interface. Do not +assume that schemas from different Core versions are compatible, and never use an unpinned image tag for this work. ## Before you start -The tutorial assumes the following: +Treat this as a manual, high-risk database migration. Plan and validate the procedure with your SuperTokens support contact or a maintainer +who understands the released Core storage schemas. Do not test it directly on production data. + +Do not begin the production migration until the following are true: -- You have access to both your `MySQL` and `PostgreSQL` databases -- Both databases are running on the same version of **SuperTokens Core** -- You have administrative privileges on both databases +- Record the exact source Core image tag, storage schema version, database name, schema, and custom table prefix. +- Initialize the PostgreSQL schema with the exact same Core version and the same custom table prefix. Upgrade Core only + as a separate, subsequently tested operation. +- Use a new target with no pre-existing SuperTokens application data. Never merge the source into a target that has + accepted writes. +- Keep both databases and Core instances on private networks. Do not expose either database or Core publicly during the + migration. +- Prove that a MySQL backup can be restored before the maintenance window. After writes stop, take the final source + backup. Also define how the PostgreSQL target will be reset if an attempt fails. +- Schedule a write outage. Stop every Core and other process that can write to the source, and keep the target Core + stopped during import and validation. +- Define an explicit rollback point and keep MySQL as the authoritative database until every acceptance check passes. ## Steps @@ -42,7 +55,7 @@ mysqldump supertokens --fields-terminated-by ',' --fields-enclosed-by '"' --fie If you do not have permissions to write to the database filesystem you can use the following python script to export tables one by one: -## Export script + ```python #!/usr/bin/env python3 import subprocess @@ -120,6 +133,7 @@ def main(): if __name__ == "__main__": main() ``` + ::: @@ -130,12 +144,12 @@ This creates CSV files for all tables in the `/var/lib/mysql-files/` directory. The `webauthn_credentials` table requires special handling because of the data type used to store the `public_key` field. ```sql -SELECT id, app_id, rp_id, user_id, counter, HEX(public_key) AS public_key, transports, created_at, updated_at -FROM webauthn_credentials -INTO OUTFILE '/var/lib/mysql-files/webauthn_credentials_hex.txt' -FIELDS TERMINATED BY ',' -OPTIONALLY ENCLOSED BY '"' -ESCAPED BY '\\' +SELECT id, app_id, rp_id, user_id, counter, HEX(public_key) AS public_key, transports, created_at, updated_at +FROM webauthn_credentials +INTO OUTFILE '/var/lib/mysql-files/webauthn_credentials_hex.txt' +FIELDS TERMINATED BY ',' +OPTIONALLY ENCLOSED BY '"' +ESCAPED BY '\\' LINES TERMINATED BY '\n'; ``` @@ -166,15 +180,15 @@ The *With Order* shows you how to import everything one by one without triggerin For most tables, you can import the data directly. - - + + ```sql -COPY FROM '/pg-data-host/.csv' +COPY FROM '/pg-data-host/.csv' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` - + - + ```bash #!/bin/bash @@ -262,9 +276,9 @@ echo "" echo "Import complete!" ``` - + - + #### 5.3 Handle the third-party provider clients table @@ -272,9 +286,7 @@ echo "Import complete!" The `tenant_thirdparty_provider_clients` table requires special handling. You need to do this to differences between the `MySQL` `JSON` and `PostgreSQL` `text[]` formats. - - -## Create a staging table +##### 5.3.1 Create a staging table ```sql CREATE TABLE tenant_thirdparty_provider_clients_raw ( @@ -291,14 +303,14 @@ CREATE TABLE tenant_thirdparty_provider_clients_raw ( ); ``` -## Import data into the staging table: +##### 5.3.2 Import data into the staging table ```sql -COPY tenant_thirdparty_provider_clients_raw FROM '/host/tenant_thirdparty_provider_clients.txt' +COPY tenant_thirdparty_provider_clients_raw FROM '/host/tenant_thirdparty_provider_clients.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` -## Convert and insert into the final table: +##### 5.3.3 Convert and insert into the final table ```sql INSERT INTO tenant_thirdparty_provider_clients ( @@ -314,15 +326,11 @@ SELECT FROM tenant_thirdparty_provider_clients_raw; ``` - - #### 5.4 Handle the WebAuthn credentials table The `webauthn_credentials` table requires conversion from MySQL Binary Large Object, `BLOB`, to PostgreSQL Binary Data, `BYTEA` format. - - -## Create a staging table +##### 5.4.1 Create a staging table ```sql CREATE TABLE IF NOT EXISTS webauthn_credentials_staging ( @@ -338,34 +346,32 @@ CREATE TABLE IF NOT EXISTS webauthn_credentials_staging ( ); ``` -## Import the hexadecimal data +##### 5.4.2 Import the hexadecimal data ```sql -COPY webauthn_credentials_staging FROM '/host/webauthn_credentials_hex.txt' +COPY webauthn_credentials_staging FROM '/host/webauthn_credentials_hex.txt' CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N'; ``` -## Convert and insert into the final table +##### 5.4.3 Convert and insert into the final table ```sql INSERT INTO webauthn_credentials ( id, app_id, rp_id, user_id, counter, public_key, transports, created_at, updated_at ) -SELECT - id, app_id, rp_id, user_id, counter, - decode(public_key, 'hex'), - transports, created_at, updated_at +SELECT + id, app_id, rp_id, user_id, counter, + decode(public_key, 'hex'), + transports, created_at, updated_at FROM webauthn_credentials_staging; ``` - - #### 5.5 Delete the staging tables Delete the two temporary tables. -``` +```sql DROP TABLE webauthn_credentials_staging; DROP TABLE tenant_thirdparty_provider_clients_raw; ``` @@ -464,6 +470,3 @@ fi ``` If the numbers match, you have successfully migrated your SuperTokens data from `MySQL` to `PostgreSQL` :tada: - - - diff --git a/docs/deployment/rate-limits.mdx b/docs/deployment/rate-limits.mdx index de0ffb051e..dd00257e56 100644 --- a/docs/deployment/rate-limits.mdx +++ b/docs/deployment/rate-limits.mdx @@ -1,16 +1,10 @@ --- title: Rate limit policy -hide_title: true -sidebar_position: 3 -description: >- - Understand and implement SuperTokens rate limit policies for managed and - self-hosted services. -page_type: guide -category: deployment +description: Understand and implement SuperTokens rate limit policies for managed and self-hosted services. +sidebar: + order: 3 --- -# Rate limit policy - ## Overview The following page describes how rate limits apply during SuperTokens API calls. @@ -25,15 +19,14 @@ However, if you query the core using different IP addresses or for a different a The free tier of the managed service has a rate limit of 50 requests per second with a burst of 50 requests per second (with no delay). This should be enough for 5-10 concurrent sign in / up (each sign in API call queries the SuperTokens core multiple times). -:::important -The backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error). +:::note[The backend SDK auto retries if it gets a `429` status code from the core (up to 5 times before throwing an error).] ::: ### Paying users If you are a paying user for SuperTokens, the rate limit and the burst limit adjust dynamically based on your usage (with a minimum of a 100rps). You should not see any `429`s unless there is a **significant** spike in requests. -:::info Paid Feature +:::info[Paid Feature] If you want higher rate limits, please [email support](mailto:support@supertokens.com), requesting a higher rate limit. ::: diff --git a/docs/deployment/scalability.mdx b/docs/deployment/scalability.mdx index 9c64ebc736..26fcfb3bfa 100644 --- a/docs/deployment/scalability.mdx +++ b/docs/deployment/scalability.mdx @@ -1,17 +1,10 @@ --- -id: scalability title: Scalability -hide_title: true -sidebar_position: 4 -description: >- - Learn how SuperTokens scales to handle millions of users and requests - efficiently. -page_type: guide -category: deployment +description: Learn how SuperTokens scales to handle millions of users and requests efficiently. +sidebar: + order: 4 --- -# Scalability - ## Overview The following page addresses how the **SuperTokens** components scale based on different factors. @@ -29,8 +22,7 @@ As the number of tenants scales, it does not affect performance on most operatio ## SuperTokens core -:::important -If you are using the managed service, the SuperTokens core is fully managed, and you don't have to worry about scaling it. +:::note[If you are using the managed service, the SuperTokens core is fully managed, and you don't have to worry about scaling it.] This section is for those who are self-hosting the SuperTokens core service. ::: @@ -49,11 +41,11 @@ The scale of end users that this can support is in the order of 1-2 million mont ### Average latency over 1 day - + ### Number of requests per minute over 1 day - + ### Performance tuning diff --git a/docs/deployment/self-host-supertokens.mdx b/docs/deployment/self-host-supertokens.mdx index 98d38f7036..c45c7b9987 100644 --- a/docs/deployment/self-host-supertokens.mdx +++ b/docs/deployment/self-host-supertokens.mdx @@ -1,26 +1,27 @@ --- title: Self-host SuperTokens -hide_title: true -sidebar_position: 1 -toc_max_heading_level: 4 -description: >- - Run SuperTokens on your infrastructure, control authentication data, and scale - as needed. -page_type: tutorial -category: deployment +description: Deploy SuperTokens Core privately with Docker or on a VM and connect PostgreSQL for persistent storage. +sidebar: + order: 1 --- + -# Self-hosting SuperTokens +## Self-hosting summary - - See how you can run **SuperTokens** in your own infrastructure. - +- Deploy Core with its Docker image or directly on a VM. +- Core 11.0.0 dropped MySQL and MongoDB support; an in-memory database is available for testing. Confirm the supported PostgreSQL range for your exact release. +- Core listens on port 3567 by default. `/hello` normally performs a storage read, but rate-limited responses can return 200 without one; it is not a complete database-health or security check. +- Docker accepts either `POSTGRESQL_CONNECTION_URI` or separate PostgreSQL host, port, database, username, and password variables. - + +See how you can run **SuperTokens** in your own infrastructure. + +--- -## Overview + +## Overview One of the main features of **SuperTokens** is that you can run it using your own resources. This way you have full control over the authentication data and you can scale based on your needs. @@ -29,12 +30,21 @@ This way you have full control over the authentication data and you can scale ba To deploy the Core Service you must configure two things: the actual API and the database. - The core service can be deployed using a **Docker** image or directly inside your VM. -- The supported database is **PostgreSQL**. The minimum required version is `13.0`. -- The core service is a **trusted backend component**. Deploy it on a private network reachable only by your backend, and never expose it directly to the public internet or to your frontend. When it must be reachable more widely, protect it with an [API key](/docs/platform-configuration/supertokens-core/api-keys). See [Secure the core](#secure-the-core) for details. +- The supported database is **PostgreSQL**. Confirm the supported version range for the exact Core/database-plugin release you select. + +:::danger +SuperTokens Core is a trusted backend component. It exposes APIs that can administer users, sessions, and tenants. Run +Core and PostgreSQL on private networks reachable only by trusted backend services; never expose either directly to a +browser or any client you do not trust. Core has no API key by default. Configure a generated API key, firewall/security-group +rules, and TLS at a trusted proxy or load balancer as defense in depth. Tenant isolation must be enforced by your backend; +a shared Core API key does not authorize an end user for a tenant. See [Secure the core](#secure-the-core) for details. +::: -:::info -**SuperTokens Core** has dropped **MySQL** and **MongoDB** support with the `11.0.0` release. -If you want to reference the old documentation, please [open this page](/docs/legacy/core/v10/self-host-supertokens). +The exact PostgreSQL support range and current Core/database artifact mapping are not established by this guide. Verify +both for the immutable release selected for production. + +:::info[**SuperTokens Core** has dropped **MySQL** and **MongoDB** support with the `11.0.0` release.] +If you want to reference the old documentation, please [open this page](/legacy/core/v10/self-host-supertokens). ::: @@ -43,89 +53,93 @@ If you want to reference the old documentation, please [open this page](/docs/le ### 1. Install SuperTokens core -#### With docker +#### With Docker +Do not use an untagged image or `latest`. Select and verify an exact supported Core image, pin it by digest, and set it as +`SUPERTOKENS_IMAGE`. For a local-only in-memory test, bind Core to `127.0.0.1`: ```bash -docker run -p 3567:3567 -d supertokens/supertokens-postgresql:latest +: "${SUPERTOKENS_IMAGE:?Set an immutable image reference such as repository:version@sha256:digest}" +docker run -p 127.0.0.1:3567:3567 -d "$SUPERTOKENS_IMAGE" ``` -- To see all the environment variables available, please see [the README file](https://github.com/supertokens/supertokens-docker-postgresql/blob/master/README.md). -- The above command starts the container with an in-memory database. This means you **do not need to connect it to PostgreSQL to test out SuperTokens**. +Omitting PostgreSQL configuration starts the container with an in-memory database. Use this only for testing. -#### Without docker +#### Without Docker ##### 1. Download SuperTokens - ## Visit the [open source download page](https://SuperTokens.com/use-oss). - ## Click on the "Binary" tab. - ## Choose your database. - ## Download the SuperTokens zip file for your OS. + +Open the [open source download page](https://SuperTokens.com/use-oss). + + + + + + + -Once downloaded, remove the zip, and you see a folder named `supertokens`. +After downloading, verify the release checksum or signature and extract the archive. You should see a folder named `supertokens`. ##### 2. Install SuperTokens - - - + + ```bash -# sudo is required so that the supertokens +# sudo is required so that the supertokens # command can be added to your PATH variable. cd supertokens sudo ./install ``` - - - - + + ```bash cd supertokens ./install ``` -:::caution -You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above. -::: - - - - - + + ```batch -Rem run as an Administrator. This is required so that the supertokens +Rem run as an Administrator. This is required so that the supertokens Rem command can be added to your PATH. cd supertokens install.bat ``` - - + + + + + +:::warning[You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above.] +::: + + -:::important -After installing, you can delete the downloaded folder as you no longer need it. +:::note[After installing, you can delete the downloaded folder as you no longer need it.] Make any changes to the configuration in the `config.yaml` file in the installation directory, as specified in the output of the `supertokens --help` command. ::: -##### 3. Start the core service +##### 3. Start the core service Running the following command starts the service. ```bash supertokens start [--host=...] [--port=...] ``` -- The above command starts the container with an in-memory database. +- The above command starts the Core service using the configured database. - To see all available options please run `supertokens start --help` -:::info Tip +:::info[Tip] To stop the service, run the following command: ```bash supertokens stop @@ -133,102 +147,107 @@ supertokens stop ::: -### 2. Test that the service is running +### 2. Test that the service is running Open a browser and visit `http://localhost:3567/hello`. If you see a page that says `Hello` back, then the container started successfully! If you are having issues with starting the docker image, please feel free to reach out [over email](mailto:support@supertokens.com) or [via Discord](https://supertokens.com/discord). :::tip -The `/hello` route checks whether the database connection is correctly set up and only returns a 200 status code if there is no issue. - -If you are using Kubernetes or docker swarm, this endpoint is perfect for doing readiness and liveness probes. +`/hello` normally performs a storage read and returns an error if that read fails. However, after its request-rate limit is +exhausted, it can return `200 Hello` without querying storage. It also deliberately requires no API key. Use it only as a +basic process/readiness signal, not as proof of database health, API-key enforcement, or safe network exposure. Pair it +with authenticated application checks and database monitoring; tune liveness separately to avoid restart loops. ::: +### 3. Connect the backend SDK with SuperTokens -### 3. Connect the backend SDK with SuperTokens - -- The default `port` for SuperTokens is `3567`. You can change this by binding a different port in the `docker run` command. For example, `docker run -p 8080:3567` runs SuperTokens on port `8080` on your machine. +- The default port for SuperTokens is `3567`. Keep it private. For local testing, bind it only to `127.0.0.1`, for example `-p 127.0.0.1:8080:3567`. - The connection info goes in the `supertokens` object in the `init` function on your backend: - - - + + ```tsx import supertokens from "supertokens-node"; +const apiKey = process.env.SUPERTOKENS_API_KEY; +if (apiKey === undefined || apiKey.length === 0) { + throw new Error("SUPERTOKENS_API_KEY is required"); +} + supertokens.init({ - // highlight-start - supertokens: { - connectionURI: "http://localhost:3567", - apiKey: "someKey" // OR can be undefined - }, - // highlight-end - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [] + supertokens: { + connectionURI: "http://localhost:3567", + apiKey, + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], }); ``` - - - - + + ```go -import "github.com/supertokens/supertokens-golang/supertokens" +import ( + "os" + + "github.com/supertokens/supertokens-golang/supertokens" +) func main() { + apiKey := os.Getenv("SUPERTOKENS_API_KEY") + if apiKey == "" { + panic("SUPERTOKENS_API_KEY is required") + } supertokens.Init(supertokens.TypeInput{ - // highlight-start Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:3567", - APIKey: "someKey", + APIKey: apiKey, }, - // highlight-end }) } ``` + + +```python check=false reason="Partial configuration example" +import os - - - -```python from supertokens_python import init, InputAppInfo, SupertokensConfig +api_key = os.environ["SUPERTOKENS_API_KEY"] +if not api_key: + raise RuntimeError("SUPERTOKENS_API_KEY is required") + init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - # highlight-start supertokens_config=SupertokensConfig( connection_uri='http://localhost:3567', - api_key='someKey' + api_key=api_key ), - # highlight-end - framework='...', # type: ignore + framework='...', recipe_list=[ #... ] ) ``` + + - - - -:::caution Security -The SuperTokens Core is a backend service, meant to be accessed only by your own backend through the backend SDKs. It must **not** be exposed directly to the public internet, to your frontend, or to end users. Any caller that can reach the Core can perform administrative operations on your users' data. - -- **No API key exists by default.** Whenever the Core is reachable by anything other than your backend, protect it with an [API key](/docs/platform-configuration/supertokens-core/api-keys). -- **Prefer network isolation.** Deploy the Core on a private network (for example a VPC or an internal subnet) reachable only by your backend. -- **Add further controls as needed:** restrict access by [IP address](/docs/platform-configuration/supertokens-core/ip-allow-deny) and serve traffic over [TLS/SSL](/docs/platform-configuration/supertokens-core/add-ssl-via-nginx). +:::info[Configure the same generated secret in Core and every backend] +Generate a key with `openssl rand -hex 32`, store it in your secret manager, and inject it as Core's `API_KEYS` and the +`SUPERTOKENS_API_KEY` used by the backend. Never bake it into source code or an image. See the +[API-key documentation](/platform-configuration/supertokens-core/api-keys) for validation and rotation. ::: ### 4. Set up the database -#### 4.1 Create a database {{optional}} +#### 4.1 Create a database (optional) ```sql @@ -239,49 +258,47 @@ CREATE DATABASE supertokens; You can skip this step if you want SuperTokens to write to your own database. In this case, you need to provide your database's name as shown in the step below. -#### 4.2 Connect SuperTokens to your database - -##### With docker +#### 4.2 Connect SuperTokens to your database +##### With Docker -:::caution -Host being `localhost` / `127.0.0.1` does not work in a docker image. Instead, please provide the database's local / public host name or IP address. -You also need to make the database listen on all the IPs of the local machine. -Edit the `postgresql.conf` configuration file and set the value of `listen_addresses` to `0.0.0.0`. +:::warning +Inside a container, `localhost` refers to that container. Attach Core and PostgreSQL to the same private Docker network, +or use a private DNS name/interface. Configure PostgreSQL `listen_addresses`, `pg_hba.conf`, host firewall, and cloud +security groups so that only Core can connect. Do not publish port 5432 or expose the database through a public address. ::: -:::caution -It is important to use the `postgresql://` scheme designator in the PostgreSQL Connection URI. Using `postgres://` will lead to a startup error. +:::warning[It is important to use the `postgresql://` scheme designator in the PostgreSQL Connection URI. Using `postgres://` will lead to a startup error.] ::: ```bash +: "${SUPERTOKENS_IMAGE:?Set an immutable Core image reference}" +: "${SUPERTOKENS_API_KEY:?Set a generated Core API key}" docker run \ - -p 3567:3567 \ - // highlight-next-line + --network app-network \ -e POSTGRESQL_CONNECTION_URI="postgresql://username:pass@host/dbName" \ - -d supertokens/supertokens-postgresql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d "$SUPERTOKENS_IMAGE" # OR docker run \ - -p 3567:3567 \ - // highlight-start + --network app-network \ -e POSTGRESQL_USER="username" \ -e POSTGRESQL_PASSWORD="password" \ -e POSTGRESQL_HOST="host" \ -e POSTGRESQL_PORT="5432" \ -e POSTGRESQL_DATABASE_NAME="supertokens" \ - // highlight-end - -d supertokens/supertokens-postgresql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d "$SUPERTOKENS_IMAGE" ``` -:::tip -You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option. +:::tip[You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option.] ::: -##### Without docker +##### Without Docker ```yaml @@ -306,662 +323,63 @@ postgresql_database_name: "supertokens" You can also provide the table schema by setting the `postgresql_table_schema` option. - :::info -The required tables should create automatically if the database user has table creation permission. -If not, you can create them manually using the following snippet. - - - ## Database tables - -```sql -CREATE TABLE apps ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT, - CONSTRAINT apps_pkey PRIMARY KEY (app_id) -); - -CREATE TABLE tenants ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT, - CONSTRAINT tenants_pkey PRIMARY KEY (app_id, tenant_id), - CONSTRAINT tenants_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX tenants_app_id_index ON tenants (app_id); - -CREATE TABLE tenant_configs ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - core_config TEXT, - email_password_enabled BOOLEAN, - passwordless_enabled BOOLEAN, - third_party_enabled BOOLEAN, - is_first_factors_null BOOLEAN, - CONSTRAINT tenant_configs_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id) -); - -CREATE TABLE tenant_thirdparty_providers ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - name VARCHAR(64), - authorization_endpoint TEXT, - authorization_endpoint_query_params TEXT, - token_endpoint TEXT, - token_endpoint_body_params TEXT, - user_info_endpoint TEXT, - user_info_endpoint_query_params TEXT, - user_info_endpoint_headers TEXT, - jwks_uri TEXT, - oidc_discovery_endpoint TEXT, - require_email BOOLEAN, - user_info_map_from_id_token_payload_user_id VARCHAR(64), - user_info_map_from_id_token_payload_email VARCHAR(64), - user_info_map_from_id_token_payload_email_verified VARCHAR(64), - user_info_map_from_user_info_endpoint_user_id VARCHAR(64), - user_info_map_from_user_info_endpoint_email VARCHAR(64), - user_info_map_from_user_info_endpoint_email_verified VARCHAR(64), - CONSTRAINT tenant_thirdparty_providers_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id), - CONSTRAINT tenant_thirdparty_providers_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX tenant_thirdparty_providers_tenant_id_index ON tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE tenant_thirdparty_provider_clients ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - client_type VARCHAR(64) DEFAULT '' NOT NULL, - client_id VARCHAR(256) NOT NULL, - client_secret TEXT, - scope VARCHAR(128)[], - force_pkce BOOLEAN, - additional_config TEXT, - CONSTRAINT tenant_thirdparty_provider_clients_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type), - CONSTRAINT tenant_thirdparty_provider_clients_third_party_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id, third_party_id) REFERENCES public.tenant_thirdparty_providers(connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE -); - -CREATE INDEX tenant_thirdparty_provider_clients_third_party_id_index ON tenant_thirdparty_provider_clients (connection_uri_domain, app_id, tenant_id, third_party_id); - -CREATE TABLE tenant_first_factors ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - factor_id VARCHAR(128), - CONSTRAINT tenant_first_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), - CONSTRAINT tenant_first_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS tenant_first_factors_tenant_id_index ON tenant_first_factors (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE tenant_required_secondary_factors ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - factor_id VARCHAR(128), - CONSTRAINT tenant_required_secondary_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), - CONSTRAINT tenant_required_secondary_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON tenant_required_secondary_factors (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE key_value ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - name VARCHAR(128) NOT NULL, - value TEXT, - created_at_time BIGINT, - CONSTRAINT key_value_pkey PRIMARY KEY (app_id, tenant_id, name), - CONSTRAINT key_value_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX key_value_tenant_id_index ON key_value (app_id, tenant_id); - -CREATE TABLE app_id_to_user_id ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - recipe_id VARCHAR(128) NOT NULL, - primary_or_recipe_user_id CHAR(36) NOT NULL, - is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, - CONSTRAINT app_id_to_user_id_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, - CONSTRAINT app_id_to_user_id_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX app_id_to_user_id_app_id_index ON app_id_to_user_id (app_id); - -CREATE INDEX app_id_to_user_id_primary_user_id_index ON app_id_to_user_id (primary_or_recipe_user_id, app_id); - -CREATE TABLE all_auth_recipe_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - primary_or_recipe_user_id CHAR(36) NOT NULL, - is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, - recipe_id VARCHAR(128) NOT NULL, - time_joined BIGINT NOT NULL, - primary_or_recipe_user_time_joined BIGINT NOT NULL, - CONSTRAINT all_auth_recipe_users_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT all_auth_recipe_users_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, - CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES public.app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, - CONSTRAINT all_auth_recipe_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users - (app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users - (app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users - (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - - -CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users - (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); - - -CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users - (primary_or_recipe_user_id, app_id); - -CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users - (app_id, recipe_id, tenant_id); - -CREATE INDEX all_auth_recipe_user_id_index ON all_auth_recipe_users (app_id, user_id); - -CREATE INDEX all_auth_recipe_tenant_id_index ON all_auth_recipe_users (app_id, tenant_id); - -CREATE TABLE userid_mapping ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - supertokens_user_id character(36) NOT NULL, - external_user_id VARCHAR(128) NOT NULL, - external_user_id_info TEXT, - CONSTRAINT userid_mapping_external_user_id_key UNIQUE (app_id, external_user_id), - CONSTRAINT userid_mapping_pkey PRIMARY KEY (app_id, supertokens_user_id, external_user_id), - CONSTRAINT userid_mapping_supertokens_user_id_key UNIQUE (app_id, supertokens_user_id), - CONSTRAINT userid_mapping_supertokens_user_id_fkey FOREIGN KEY (app_id, supertokens_user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX userid_mapping_supertokens_user_id_index ON userid_mapping (app_id, supertokens_user_id); - -CREATE TABLE dashboard_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - password_hash VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT dashboard_users_email_key UNIQUE (app_id, email), - CONSTRAINT dashboard_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT dashboard_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX dashboard_users_app_id_index ON dashboard_users (app_id); - -CREATE TABLE dashboard_user_sessions ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - session_id character(36) NOT NULL, - user_id character(36) NOT NULL, - time_created BIGINT NOT NULL, - expiry BIGINT NOT NULL, - CONSTRAINT dashboard_user_sessions_pkey PRIMARY KEY (app_id, session_id), - CONSTRAINT dashboard_user_sessions_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.dashboard_users(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX dashboard_user_sessions_expiry_index ON dashboard_user_sessions (expiry); - -CREATE INDEX dashboard_user_sessions_user_id_index ON dashboard_user_sessions (app_id, user_id); - -CREATE TABLE session_access_token_signing_keys ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT NOT NULL, - value TEXT, - CONSTRAINT session_access_token_signing_keys_pkey PRIMARY KEY (app_id, created_at_time), - CONSTRAINT session_access_token_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX access_token_signing_keys_app_id_index ON session_access_token_signing_keys (app_id); - -CREATE TABLE session_info ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - session_handle VARCHAR(255) NOT NULL, - user_id VARCHAR(128) NOT NULL, - refresh_token_hash_2 VARCHAR(128) NOT NULL, - session_data TEXT, - expires_at BIGINT NOT NULL, - created_at_time BIGINT NOT NULL, - jwt_user_payload TEXT, - use_static_key BOOLEAN NOT NULL, - CONSTRAINT session_info_pkey PRIMARY KEY (app_id, tenant_id, session_handle), - CONSTRAINT session_info_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX session_expiry_index ON session_info (expires_at); - -CREATE INDEX session_info_tenant_id_index ON session_info (app_id, tenant_id); - -CREATE TABLE user_last_active ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - last_active_time BIGINT, - CONSTRAINT user_last_active_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT user_last_active_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX user_last_active_app_id_index ON user_last_active (app_id); - -CREATE TABLE emailpassword_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - password_hash VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT emailpassword_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT emailpassword_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE emailpassword_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - CONSTRAINT emailpassword_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), - CONSTRAINT emailpassword_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT emailpassword_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE emailpassword_pswd_reset_tokens ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - token VARCHAR(128) NOT NULL, - token_expiry BIGINT NOT NULL, - email VARCHAR(256), - CONSTRAINT emailpassword_pswd_reset_tokens_pkey PRIMARY KEY (app_id, user_id, token), - CONSTRAINT emailpassword_pswd_reset_tokens_token_key UNIQUE (token), - CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX emailpassword_password_reset_token_expiry_index ON emailpassword_pswd_reset_tokens (token_expiry); - -CREATE INDEX emailpassword_pswd_reset_tokens_user_id_index ON emailpassword_pswd_reset_tokens (app_id, user_id); - -CREATE TABLE emailverification_verified_emails ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - email VARCHAR(256) NOT NULL, - CONSTRAINT emailverification_verified_emails_pkey PRIMARY KEY (app_id, user_id, email), - CONSTRAINT emailverification_verified_emails_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX emailverification_verified_emails_app_id_index ON emailverification_verified_emails (app_id); - -CREATE TABLE emailverification_tokens ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - email VARCHAR(256) NOT NULL, - token VARCHAR(128) NOT NULL, - token_expiry BIGINT NOT NULL, - CONSTRAINT emailverification_tokens_pkey PRIMARY KEY (app_id, tenant_id, user_id, email, token), - CONSTRAINT emailverification_tokens_token_key UNIQUE (token), - CONSTRAINT emailverification_tokens_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX emailverification_tokens_index ON emailverification_tokens (token_expiry); - -CREATE INDEX emailverification_tokens_tenant_id_index ON emailverification_tokens (app_id, tenant_id); - -CREATE TABLE thirdparty_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - third_party_user_id VARCHAR(256) NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT thirdparty_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT thirdparty_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX thirdparty_users_email_index ON thirdparty_users (app_id, email); - -CREATE INDEX thirdparty_users_thirdparty_user_id_index ON thirdparty_users (app_id, third_party_id, third_party_user_id); - -CREATE TABLE thirdparty_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - third_party_user_id VARCHAR(256) NOT NULL, - CONSTRAINT thirdparty_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT thirdparty_user_to_tenant_third_party_user_id_key UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id), - CONSTRAINT thirdparty_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - time_joined BIGINT NOT NULL, - CONSTRAINT passwordless_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT passwordless_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - CONSTRAINT passwordless_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), - CONSTRAINT passwordless_user_to_tenant_phone_number_key UNIQUE (app_id, tenant_id, phone_number), - CONSTRAINT passwordless_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT passwordless_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_devices ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - device_id_hash character(44) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - link_code_salt character(44) NOT NULL, - failed_attempts integer NOT NULL, - CONSTRAINT passwordless_devices_pkey PRIMARY KEY (app_id, tenant_id, device_id_hash), - CONSTRAINT passwordless_devices_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX passwordless_devices_email_index ON passwordless_devices (app_id, tenant_id, email); - -CREATE INDEX passwordless_devices_phone_number_index ON passwordless_devices (app_id, tenant_id, phone_number); - -CREATE INDEX passwordless_devices_tenant_id_index ON passwordless_devices (app_id, tenant_id); - -CREATE TABLE passwordless_codes ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - code_id character(36) NOT NULL, - device_id_hash character(44) NOT NULL, - link_code_hash character(44) NOT NULL, - created_at BIGINT NOT NULL, - CONSTRAINT passwordless_codes_link_code_hash_key UNIQUE (app_id, tenant_id, link_code_hash), - CONSTRAINT passwordless_codes_pkey PRIMARY KEY (app_id, tenant_id, code_id), - CONSTRAINT passwordless_codes_device_id_hash_fkey FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES public.passwordless_devices(app_id, tenant_id, device_id_hash) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX passwordless_codes_created_at_index ON passwordless_codes (app_id, tenant_id, created_at); - -CREATE INDEX passwordless_codes_device_id_hash_index ON passwordless_codes (app_id, tenant_id, device_id_hash); - -CREATE TABLE jwt_signing_keys ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - key_id VARCHAR(255) NOT NULL, - key_string TEXT NOT NULL, - algorithm VARCHAR(10) NOT NULL, - created_at BIGINT, - CONSTRAINT jwt_signing_keys_pkey PRIMARY KEY (app_id, key_id), - CONSTRAINT jwt_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX jwt_signing_keys_app_id_index ON jwt_signing_keys (app_id); - -CREATE TABLE user_metadata ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - user_metadata TEXT NOT NULL, - CONSTRAINT user_metadata_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT user_metadata_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX user_metadata_app_id_index ON user_metadata (app_id); - -CREATE TABLE roles ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - role VARCHAR(255) NOT NULL, - CONSTRAINT roles_pkey PRIMARY KEY (app_id, role), - CONSTRAINT roles_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX roles_app_id_index ON roles (app_id); - -CREATE TABLE role_permissions ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - role VARCHAR(255) NOT NULL, - permission VARCHAR(255) NOT NULL, - CONSTRAINT role_permissions_pkey PRIMARY KEY (app_id, role, permission), - CONSTRAINT role_permissions_role_fkey FOREIGN KEY (app_id, role) REFERENCES public.roles(app_id, role) ON DELETE CASCADE -); - -CREATE INDEX role_permissions_permission_index ON role_permissions (app_id, permission); - -CREATE INDEX role_permissions_role_index ON role_permissions (app_id, role); - -CREATE TABLE user_roles ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - role VARCHAR(255) NOT NULL, - CONSTRAINT user_roles_pkey PRIMARY KEY (app_id, tenant_id, user_id, role), - CONSTRAINT user_roles_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX user_roles_role_index ON user_roles (app_id, tenant_id, role); - -CREATE INDEX user_roles_tenant_id_index ON user_roles (app_id, tenant_id); - -CREATE INDEX user_roles_app_id_role_index ON user_roles (app_id, role); - -CREATE TABLE totp_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - CONSTRAINT totp_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT totp_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX totp_users_app_id_index ON totp_users (app_id); - -CREATE TABLE totp_user_devices ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - device_name VARCHAR(256) NOT NULL, - secret_key VARCHAR(256) NOT NULL, - period integer NOT NULL, - skew integer NOT NULL, - verified BOOLEAN NOT NULL, - created_at BIGINT, - CONSTRAINT totp_user_devices_pkey PRIMARY KEY (app_id, user_id, device_name), - CONSTRAINT totp_user_devices_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX totp_user_devices_user_id_index ON totp_user_devices (app_id, user_id); - -CREATE TABLE totp_used_codes ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - code VARCHAR(8) NOT NULL, - is_valid BOOLEAN NOT NULL, - expiry_time_ms BIGINT NOT NULL, - created_time_ms BIGINT NOT NULL, - CONSTRAINT totp_used_codes_pkey PRIMARY KEY (app_id, tenant_id, user_id, created_time_ms), - CONSTRAINT totp_used_codes_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, - CONSTRAINT totp_used_codes_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX totp_used_codes_expiry_time_ms_index ON totp_used_codes (app_id, tenant_id, expiry_time_ms); - -CREATE INDEX totp_used_codes_tenant_id_index ON totp_used_codes (app_id, tenant_id); - -CREATE INDEX totp_used_codes_user_id_index ON totp_used_codes (app_id, user_id); - -CREATE TABLE IF NOT EXISTS bulk_import_users ( - id CHAR(36), - app_id VARCHAR(64) NOT NULL DEFAULT 'public', - primary_user_id VARCHAR(36), - raw_data TEXT NOT NULL, - status VARCHAR(128) DEFAULT 'NEW', - error_msg TEXT, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, - CONSTRAINT bulk_import_users_pkey PRIMARY KEY(app_id, id), - CONSTRAINT bulk_import_users__app_id_fkey FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON bulk_import_users (app_id, status, updated_at); - -CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index1 ON bulk_import_users (app_id, status, created_at DESC, id DESC); - -CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index2 ON bulk_import_users (app_id, created_at DESC, id DESC); - -CREATE INDEX IF NOT EXISTS session_info_user_id_app_id_index ON session_info (user_id, app_id); - -CREATE INDEX IF NOT EXISTS emailverification_verified_emails_app_id_email_index ON emailverification_verified_emails -(app_id, email); - -CREATE TABLE IF NOT EXISTS webauthn_account_recovery_tokens ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id CHAR(36) NOT NULL, - email VARCHAR(256) NOT NULL, - token VARCHAR(256) NOT NULL, - expires_at BIGINT NOT NULL, - CONSTRAINT webauthn_account_recovery_token_pkey PRIMARY KEY (app_id, tenant_id, user_id, token), - CONSTRAINT webauthn_account_recovery_token_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES - all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS webauthn_credentials ( - id VARCHAR(256) NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - rp_id VARCHAR(256) NOT NULL, - user_id CHAR(36), - counter BIGINT NOT NULL, - public_key BYTEA NOT NULL, - transports TEXT NOT NULL, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, - CONSTRAINT webauthn_credentials_pkey PRIMARY KEY (app_id, rp_id, id), - CONSTRAINT webauthn_credentials_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES webauthn_users - (app_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS webauthn_generated_options ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public'NOT NULL, - id CHAR(36) NOT NULL, - challenge VARCHAR(256) NOT NULL, - email VARCHAR(256), - rp_id VARCHAR(256) NOT NULL, - rp_name VARCHAR(256) NOT NULL, - origin VARCHAR(256) NOT NULL, - expires_at BIGINT NOT NULL, - created_at BIGINT NOT NULL, - user_presence_required BOOLEAN DEFAULT false NOT NULL, - user_verification VARCHAR(12) DEFAULT 'preferred' NOT NULL, - CONSTRAINT webauthn_generated_options_pkey PRIMARY KEY (app_id, tenant_id, id), - CONSTRAINT webauthn_generated_options_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES tenants - (app_id, tenant_id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS webauthn_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id CHAR(36) NOT NULL, - email VARCHAR(256) NOT NULL, - CONSTRAINT webauthn_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), - CONSTRAINT webauthn_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT webauthn_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES - all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS webauthn_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id CHAR(36) NOT NULL, - email VARCHAR(256) NOT NULL, - rp_id VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT webauthn_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT webauthn_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES app_id_to_user_id(app_id, - user_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS webauthn_user_to_tenant_email_index ON webauthn_user_to_tenant (app_id, email); -CREATE INDEX IF NOT EXISTS webauthn_user_challenges_expires_at_index ON webauthn_generated_options (app_id, tenant_id, expires_at); -CREATE INDEX IF NOT EXISTS webauthn_credentials_user_id_index ON webauthn_credentials (user_id); -CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_token_index ON webauthn_account_recovery_tokens (app_id, tenant_id, token); -CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_expires_at_index ON webauthn_account_recovery_tokens (expires_at DESC); -CREATE INDEX IF NOT EXISTS webauthn_account_recovery_token_email_index ON webauthn_account_recovery_tokens (app_id, tenant_id, email); - - -``` - - - +Core creates and migrates its required tables automatically when the database principal has DDL permission. Do not use a +hand-copied schema: it can drift from the selected Core and database-plugin release. If your production principal cannot +perform DDL, obtain a schema or migration artifact generated for the exact immutable release, apply it with a separate +migration principal, and test Core startup before promotion. ::: -#### 4.3 Test the connection +#### 4.3 Test the connection -To test, start SuperTokens and run the following query in your database +Start the exact Core release against a staging copy of the database and require startup/migration success. Then exercise +an authenticated SDK operation. A query against one table does not prove that every required migration was applied. -```sql -SELECT * FROM key_value; -``` -If you see at least one row, it means that the connection has been successfully completed! +#### 4.4 Rename database tables (optional) -#### 4.4 Rename database tables {{optional}} - -:::caution -If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one. +:::warning[If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one.] ::: -You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. +You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. For example, two tables created by SuperTokens have the names `emailpassword_users` and `thirdparty_users`. If you add a prefix to them (something like `"my_prefix"`), then the tables become `my_prefix_emailpassword_users` and `my_prefix_thirdparty_users`. - - - + + ```bash docker run \ - -p 3567:3567 \ - // highlight-next-line - -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ - -d supertokens/supertokens-postgresql + --network app-network \ + -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d "$SUPERTOKENS_IMAGE" ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_table_names_prefix: "my_prefix" ``` - - + + ### 5. Add license keys To access some features in your self-hosted service you must use **license keys**. -You can sign up on [**SuperTokens**](https://supertokens.com/auth) to receive one. +You can sign up on [**SuperTokens**](https://supertokens.com/auth) to receive one. Once you have the license key you need to manually add it to your **SuperTokens Core Instance**. To do this you have to call the Core API with the following request: -```bash title="Add License Key" showAppTypeSelect -curl --location --request PUT ^{coreInfo.uri}/ee/license \ +```bash title="Add License Key" +curl --location --request PUT "${CORE_API_ENDPOINT:?Set the private Core endpoint}/ee/license" \ --header 'Content-Type: application/json' \ - --header 'api-key: ^{coreInfo.key}' \ - --data-raw '{ "licenseKey": "" }' + --header "api-key: ${SUPERTOKENS_API_KEY:?Set the Core API key}" \ + --data-raw "{ \"licenseKey\": \"${SUPERTOKENS_LICENSE_KEY:?Set the license key}\" }" ``` @@ -972,68 +390,6 @@ The SuperTokens Core exposes administrative operations over its API — creating This trust model means the Core must be treated like your database: reachable only by your own backend, never by untrusted clients. - **Isolate the network.** Run the Core on a private network or subnet that only your backend can reach. This is the primary protection and applies regardless of any other setting. -- **Set an API key.** If the Core can be reached by anything beyond your backend, configure an [API key](/docs/platform-configuration/supertokens-core/api-keys). Note that within a single deployment all callers share the same key; an API key is therefore not a substitute for network isolation between tenants. -- **Restrict by IP and use TLS.** Optionally limit access by [IP address](/docs/platform-configuration/supertokens-core/ip-allow-deny) and terminate [TLS/SSL](/docs/platform-configuration/supertokens-core/add-ssl-via-nginx) in front of the Core. -- **Enforce tenant scoping in your backend.** Sessions and other objects carry their tenant as a signed claim. When an action is restricted to a specific tenant, verify the tenant in your backend rather than relying on the URL the request arrived on. - -## References - -### Docker compose file - - - -```bash -version: '3' - -services: - # Note: If you are assigning a custom name to your db service on the line below, make sure it does not contain underscores - db: - image: 'postgres:latest' - environment: - POSTGRES_USER: supertokens_user - POSTGRES_PASSWORD: somePassword - POSTGRES_DB: supertokens - ports: - - 5432:5432 - networks: - - app_network - restart: unless-stopped - healthcheck: - test: ['CMD', 'pg_isready', '-U', 'supertokens_user', '-d', 'supertokens'] - interval: 5s - timeout: 5s - retries: 5 - - supertokens: - image: supertokens/supertokens-postgresql:latest - depends_on: - db: - condition: service_healthy - ports: - - 3567:3567 - environment: - POSTGRESQL_CONNECTION_URI: "postgresql://supertokens_user:somePassword@db:5432/supertokens" - networks: - - app_network - restart: unless-stopped - healthcheck: - test: > - bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"' - interval: 10s - timeout: 5s - retries: 5 - -networks: - app_network: - driver: bridge -``` - - -:::important -If you are running the backend process that integrates with the backend SDK as part of the docker compose file as well, make sure to use `http://supertokens:3567` as the connection URI instead of `http://localhost:3567`. -::: - -### Helm charts for Kubernetes - -- For [PostgreSQL image](https://github.com/supertokens/supertokens-docker-postgresql/tree/master/helm-chart) - +- **Set an API key.** No API key exists by default, so any caller that can reach an unprotected Core can perform administrative operations. Configure a generated [API key](/platform-configuration/supertokens-core/api-keys) as defense in depth. Core supports multiple keys for rotation, but these are not per-tenant authorization credentials and do not replace network isolation. +- **Restrict by IP and use TLS.** Limit access with firewall/security-group rules and, optionally, Core's [IP allow/deny configuration](/platform-configuration/supertokens-core/ip-allow-deny). Terminate [TLS/SSL](/platform-configuration/supertokens-core/add-ssl-via-nginx) at a trusted proxy or load balancer. +- **Enforce tenant scoping in your backend.** For session-authenticated requests restricted to a specific tenant, verify the session and check that its tenant matches the required tenant. Authorize access to tenant-specific resources in your backend; do not rely on the URL alone. diff --git a/docs/deployment/telemetry.mdx b/docs/deployment/telemetry.mdx index 1c45a37f45..3d8379efff 100644 --- a/docs/deployment/telemetry.mdx +++ b/docs/deployment/telemetry.mdx @@ -1,13 +1,10 @@ --- -sidebar_position: 6 title: OpenTelemetry Integration -hide_title: true description: Add OpenTelemetry logging to all SuperTokens APIs and function calls using the plugin -page_type: tutorial +sidebar: + order: 6 --- -# OpenTelemetry Integration - ## Overview This tutorial shows you how to add **OpenTelemetry** logging to all the **SuperTokens** APIs and function calls using the **OpenTelemetry plugin**. The guide makes use of the plugins functionality to automatically instrument your authentication flows with distributed tracing. @@ -22,7 +19,7 @@ The OpenTelemetry plugin supports only the `NodeJS` SDK. Support for other platf Make sure you have the OpenTelemetry SDK installed and configured in your application. For detailed instructions, see the [OpenTelemetry Node.js Getting Started Guide](https://opentelemetry.io/docs/languages/js/getting-started/nodejs/#instrumentation). -The implementation is in early stages and APIs might change. For more information on how plugins work, refer to the [references page](/docs/references/plugins/introduction). +The implementation is in early stages and APIs might change. For more information on how plugins work, refer to the [references page](/references/plugins/introduction). ## Steps @@ -39,13 +36,10 @@ Here's a basic configuration: ```typescript /*instrumentation.ts*/ -import { NodeSDK } from '@opentelemetry/sdk-node'; -import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node'; -import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; -import { - PeriodicExportingMetricReader, - ConsoleMetricExporter, -} from '@opentelemetry/sdk-metrics'; +import { NodeSDK } from "@opentelemetry/sdk-node"; +import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node"; +import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; +import { PeriodicExportingMetricReader, ConsoleMetricExporter } from "@opentelemetry/sdk-metrics"; const sdk = new NodeSDK({ traceExporter: new ConsoleSpanExporter(), @@ -58,7 +52,7 @@ const sdk = new NodeSDK({ sdk.start(); ``` -:::note no-title +:::note For a more thorough explanation on how to setup OpenTelemetry please refer to the [official documentation](https://opentelemetry.io/docs/languages/js/getting-started/nodejs). ::: @@ -75,16 +69,15 @@ SuperTokens.init({ connectionURI: "...", }, appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], experimental: { - plugins: [ - OpenTelemetryPlugin.init(), - ], - } + plugins: [OpenTelemetryPlugin.init()], + }, }); ``` @@ -117,7 +110,8 @@ SuperTokens.init({ connectionURI: "...", }, appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes @@ -125,17 +119,17 @@ SuperTokens.init({ experimental: { plugins: [ OpenTelemetryPlugin.init({ - override: (oI) => ({ - ...oI, - getSensitiveFields: (defaultSensitiveFields: string[]) => [ - ...defaultSensitiveFields, - "customSecretField", - "userSecret", - ], - }), + override: (oI) => ({ + ...oI, + getSensitiveFields: (defaultSensitiveFields: string[]) => [ + ...defaultSensitiveFields, + "customSecretField", + "userSecret", + ], + }), }), ], - } + }, }); ``` @@ -152,33 +146,36 @@ SuperTokens.init({ connectionURI: "...", }, appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], - plugins: [ - OpenTelemetryPlugin.init({ - override: (oI) => ({ - ...oI, - transformInputToAttributes: (input: any) => { - // Custom logic to transform input data for traces - return { - userId: input.userId, - action: input.action, - // Exclude other sensitive data - }; - }, - transformResultToAttributes: (result: any) => { - // Custom logic to transform result data for traces - return { - userId: result.userId, - // Only include non-sensitive result data - }; - }, + experimental: { + plugins: [ + OpenTelemetryPlugin.init({ + override: (oI) => ({ + ...oI, + transformInputToAttributes: (input: any) => { + // Custom logic to transform input data for traces + return { + userId: input.userId, + action: input.action, + // Exclude other sensitive data + }; + }, + transformResultToAttributes: (result: any) => { + // Custom logic to transform result data for traces + return { + userId: result.userId, + // Only include non-sensitive result data + }; + }, + }), }), - }), - ], + ], + }, }); ``` @@ -195,19 +192,22 @@ SuperTokens.init({ connectionURI: "...", }, appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes ], - plugins: [ - OpenTelemetryPlugin.init({ - override: (oI) => ({ - ...oI, - getSensitiveFields: () => [], // Disable built-in filtering + experimental: { + plugins: [ + OpenTelemetryPlugin.init({ + override: (oI) => ({ + ...oI, + getSensitiveFields: () => [], // Disable built-in filtering + }), }), - }), - ], + ], + }, }); ``` @@ -215,40 +215,20 @@ SuperTokens.init({ Besides OpenTelemetry integration, you can also look into other deployment and monitoring features: - - - - Self-Hosting - - - Self-host SuperTokens in your own infrastructure. - - - - - - Rate Limiting - - - Configure rate limits to protect your authentication endpoints. - - - - - - Scalability - - - Scale SuperTokens for high-traffic applications. - - - - - - Plugins Reference - - - General information on how plugins work. - - - + + +Self-host SuperTokens in your own infrastructure. + + + +Configure rate limits to protect your authentication endpoints. + + + +Scale SuperTokens for high-traffic applications. + + + +General information on how plugins work. + + diff --git a/docs/index.mdx b/docs/index.mdx index 4cd0730d14..711ac29caf 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,164 +1,108 @@ --- -id: index -title: Overview -sidebar_position: 1 -pagination_next: null -show_feedback: false -hide_table_of_contents: true -skip_llms: true -description: >- - Discover how you can integrate and use SuperTokens effectively in your - applications. -page_type: overview +title: Introduction +description: Add secure, customizable authentication and session management to your applications with SuperTokens. +sidebar: + order: 10 --- - -# Documentation - - - Explore our guides and examples on how you can use **SuperTokens**. - - - - -## Getting Started - - - See how you can get up and running with our product. - - - - - - Quickstart - - - Go through a quick tutorial that will show you how to integrate SuperTokens with your app. - - - - - Example Applications - - - Use our CLI to generate a boilerplate app that you can use as a starting point. - - - - - -## Features - - - Discover the different features exposed by **SuperTokens**. - - - - - - Authentication Methods - - - We support various authentication methods that you can configure to fit your needs. - - - - - Multi-Factor Authentication - - - Set up additional verification layers in your sign-in process. - - - - - - - Multi-Tenancy - - - Use SuperTokens to manage multiple organizations and apps. - - - - - - - Attack Protection Suite - - - Enable additional security features that shield your app. - - - - - - - - - Self Hosting - - - Deploy SuperTokens in your own infrastructure. - - - - - - - Migration Guide - - - Learn how to migrate from an existing authentication provider. - - - - - - - -## References - - - Get an in-depth understanding of how SDKs and APIs work. - - - - - - Frontend SDK Reference - - - Information on how the frontend SDKs are structured and how to use them. - - - - - Backend SDK Reference - - - Information on how the backend SDKs are structured and how to use them. - - - - - - FDI Reference - - - Details about the endpoints exposed by the **Frontend Driver Interface**. - This is the API enabled by the backend SDKs. - - - - - - CDI Reference - - - Details about the endpoints exposed by the **Core Driver Interface**. - This is the API enabled by the SuperTokens Core Service. - - - - +SuperTokens is an open-source authentication provider that gives you control over your user data and login experience. Use the managed service or self-host the SuperTokens Core, then integrate authentication through frontend and backend SDKs. + + + + Integrate SuperTokens into an existing application. + + + Replace an existing authentication provider with SuperTokens. + + + +## How SuperTokens works + +The **SuperTokens Core** provides the authentication functionality. Your backend SDK communicates with the Core to perform authentication tasks, while the frontend SDK sends requests to authentication routes exposed by your backend. + +Unlike many authentication providers, the frontend SDK never communicates with the authentication service directly. This lets your application control the API layer, customize authentication behavior, and keep the Core inside your infrastructure when self-hosting. + + + + + Flowchart of the SuperTokens managed service architecture + + + + + Flowchart of the self-hosted SuperTokens architecture + + +:::caution +The **SuperTokens Core** is a trusted backend component. It should only be reachable by your backend, never exposed directly to the public internet or your frontend. When self-hosting, see [Secure the core](/deployment/self-host-supertokens#secure-the-core). +::: + + + + + +### Recipes + +SuperTokens groups functionalities, like authentication methods or session and user management actions, into **recipes**. +Each can be used as extendable building blocks which allow you to customize the authentication experience based on your needs. + +## Explore capabilities + + + + Let users sign up and sign in with an email and password, with customizable forms and password reset flows. + + + Authenticate users with one-time passwords or magic links sent by email or SMS. + + + Let users sign in with Google, Apple, GitHub, and other identity providers. + + + Offer phishing-resistant sign-in using biometrics, device PINs, or security keys. + + + Authenticate users across web, mobile, and desktop applications through a common OAuth2 provider. + + + Secure service-to-service requests with access tokens using the OAuth2 client credentials flow. + + + Add email or SMS OTP, TOTP, or passkeys as factors, with step-up authentication for sensitive actions. + + + Detect risky authentication activity and respond with extra verification or blocked attempts. + + + Configure tenant-specific login methods, isolated user pools, and enterprise SSO. + + + +## Next steps + + + + Configure the sign-in methods that fit your application. + + + Protect routes, manage sessions, and work with authenticated users. + + + Give an AI coding agent the context it needs to integrate SuperTokens. + + + Use the managed service or deploy the SuperTokens Core yourself. + + diff --git a/docs/integrate-with-ai.mdx b/docs/integrate-with-ai.mdx new file mode 100644 index 0000000000..4d2f2c749e --- /dev/null +++ b/docs/integrate-with-ai.mdx @@ -0,0 +1,57 @@ +--- +title: Integrate with AI +description: Use SuperTokens documentation directly in AI chat and coding tools. +sidebar: + icon: sparkles + order: 31 +--- + +## Ask AI + +Use the **Ask AI** action in the documentation to ask questions about SuperTokens. It answers from the current page and relevant SuperTokens documentation, and includes links to the pages it used. + +## Connect an AI coding agent + +Connect your AI coding tool to the public, read-only SuperTokens documentation MCP server. The server lets an agent search and read the documentation without scraping web pages or requiring credentials. + +```bash +claude mcp add --transport http supertokens-docs https://supertokens.com/docs/mcp +``` + +Use the **Connect to MCP** action on any documentation page for client-specific setup instructions. + +The server exposes these tools: + +- `search_docs`: Search the documentation and return matching pages with excerpts. +- `get_page`: Read a documentation page as agent-optimized Markdown. +- `list_pages`: List every page with its route, title, description, and content type. +- `get_navigation`: Read the documentation navigation hierarchy. + + +## Machine-readable documentation + +### Markdown responses + +AI tools can request agent-optimized Markdown instead of HTML by sending an `Accept: text/markdown` header. This preserves the tool's context window and includes supported components as plain Markdown. + +```bash +curl -sL -H "Accept: text/markdown" https://supertokens.com/docs/integrate-with-ai +``` + +### Direct Markdown routes + +Append `.md` to a documentation URL to retrieve agent-optimized Markdown. Append `.mdx` to retrieve the original MDX source. + +For example: [integrate-with-ai.md](https://supertokens.com/docs/integrate-with-ai.md). + +Use **Copy as Markdown** to copy the current page, or **Open in chat** to open it in a supported AI assistant with the page context. + +### Site indexes + +The documentation also provides machine-readable site indexes: + +- [/llms.txt](https://supertokens.com/docs/llms.txt) is a compact, structured index of the documentation. +- [/llms-full.txt](https://supertokens.com/docs/llms-full.txt) contains the full documentation corpus. It can exceed an AI tool's context window. +- [/agent-readability.json](https://supertokens.com/docs/agent-readability.json) advertises these agent-facing documentation surfaces for automatic discovery. + +The dashboard application owns MCP discovery under `/.well-known`. Connect clients directly to the MCP URL above. diff --git a/docs/integrations/aws-lambda/appsync-integration.mdx b/docs/integrations/aws-lambda/appsync-integration.mdx new file mode 100644 index 0000000000..fa617aacd9 --- /dev/null +++ b/docs/integrations/aws-lambda/appsync-integration.mdx @@ -0,0 +1,83 @@ +--- +title: AppSync integration +description: Integrate SuperTokens with AppSync through an API Gateway Lambda authorizer. +sidebar: + order: 6 +--- + +## Overview + +A Lambda authorizer configured as described in the [authorizer guide](/integrations/aws-lambda/session-verification#using-lambda-authorizers) +can protect GraphQL HTTP operations sent from API Gateway to AppSync. + +:::warning +This architecture is not implementation-ready without a deployed IaC fixture. The exact service-integration path, +required `Host`/`Content-Type` handling, request and response mappings, GraphQL errors, and cookie behavior must be tested +for the selected API Gateway type. It does not proxy AppSync real-time WebSocket subscriptions. +::: + +## Before you start + +Configure SuperTokens in AWS Lambda by following the [AWS Lambda integration guide](/integrations/aws-lambda/quickstart-guide). + +## Steps + +### 1. Set up AppSync authorization + +Use `AWS_IAM` authorization so API Gateway signs requests with its execution role. Grant that role only +`appsync:GraphQL` for the root fields this integration needs: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "appsync:GraphQL", + "Resource": [ + "arn:aws:appsync:::apis//types/Query/fields/", + "arn:aws:appsync:::apis//types/Mutation/fields/" + ] + } + ] +} +``` + +Do not attach `AWSAppSyncInvokeFullAccess`; it includes broad read/list access and wildcard resources. This architecture +uses IAM exclusively for the API Gateway-to-AppSync hop and does not inject shared credentials into integration headers. + +### 2. Configure API Gateway with the authorizer + +Follow the [authorizer guide](/integrations/aws-lambda/session-verification#using-lambda-authorizers) to create `/auth` +and `/graphql` resources. Point `/auth` to the Lambda function that handles the auth APIs and require the Lambda +authorizer on `POST /graphql`. + +Configure `POST /graphql` as an AWS service integration that invokes only the target AppSync GraphQL API with the +least-privilege execution role above. Do not infer the current console's service, subdomain, or path-override values from +this page. Capture them in reproducible IaC and prove the generated request reaches the target API before publishing the +integration. + +### 3. Set up integration headers + +- Set the integration request's `x-user-id` header from `context.authorizer.principalId`. This must overwrite any + client-supplied `x-user-id`; never pass the incoming identity header through. +- Set the required `Content-Type` for the GraphQL request and map the request body without changing the GraphQL + document or variables. Verify these mappings in the IaC E2E fixture. + +### 4. Consume the context in resolvers + +In a VTL resolver, read the mapped user ID with: + +```text +$context.request.headers.get("x-user-id") +``` + +Treat this value as trusted only after an E2E test proves API Gateway overwrites a spoofed client header after successful +authorization. Use it for application-level ownership checks; the execution role limits which root fields API Gateway +can invoke, but does not implement per-user authorization inside a resolver. See the [resolver context documentation](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html#access-request-headers). + +### 5. Validate the deployed integration + +The required IaC fixture must cover valid, missing, expired, and invalid sessions; a spoofed `x-user-id`; IAM denial for +fields outside the allowed field list; request and response body mappings; GraphQL errors; and browser CORS behavior. +AppSync subscriptions use a separate real-time WebSocket endpoint and are outside this HTTP proxy design. diff --git a/docs/integrations/aws-lambda/meta.ts b/docs/integrations/aws-lambda/meta.ts new file mode 100644 index 0000000000..126dc2ac1b --- /dev/null +++ b/docs/integrations/aws-lambda/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "AWS Lambda", + icon: "/docs-assets/img/logos/aws-lambda-monochrome.svg", + order: 20, + collapsed: true, +}); diff --git a/docs/integrations/aws-lambda/quickstart-guide.mdx b/docs/integrations/aws-lambda/quickstart-guide.mdx new file mode 100644 index 0000000000..b40ccc225d --- /dev/null +++ b/docs/integrations/aws-lambda/quickstart-guide.mdx @@ -0,0 +1,437 @@ +--- +title: Quickstart Guide +description: Learn how to integrate SuperTokens with AWS Lambda +sidebar: + order: 1 +--- + +The following guide shows you how to use **SuperTokens** in an AWS Lambda environment. +You can also check out the [example repository](https://github.com/supertokens/supertokens-node/tree/master/examples/aws/with-emailpassword) for a full implementation. + +## Before you start + +These instructions assume that you have completed the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk). +If not, please go through it and create the example application before you start this tutorial. + +## Steps + +:::warning +Follow the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) first to learn how to set up the frontend. +::: + +### 1. Set up API Gateway + +#### 1.1 Create a REST API Gateway + +We will be using AWS API Gateway to create a REST API that will be used to communicate with our Lambda functions. + +Create API gateway step UI + +#### 1.2 Set up authentication routes + +Create an `/auth` resource and then an `/auth/{proxy+}` resource. +This will act as a catch-all for all SuperTokens auth routes. + +Create proxy route step UI + +Route creation complete step UI + +#### 1.3 Attach a Lambda function to the `ANY` method of the proxy resource + +Click on the "ANY" method and then "Integration" to configure the Lambda function. +Check **Lambda proxy integration** and then select your lambda function. + +Configure lambda integration UI + +:::note[Ensure that the **Lambda proxy integration** toggle is turned on.] + +::: + +#### 1.4 Configure CORS for the proxy path + +Click on the `{proxy+}` resource and then "Enable CORS" button to open the CORS configuration page. + +Enable CORS for the proxy path UI + + +Configure an `OPTIONS` response for `/auth/{proxy+}` with: + +- `Access-Control-Allow-Origin: `, using the exact trusted website origin. +- `Access-Control-Allow-Credentials: true`. +- `Access-Control-Allow-Headers` containing `Content-Type` and every value returned by the backend SDK's + `getAllCORSHeaders`/`get_all_cors_headers` function. +- `Access-Control-Allow-Methods` containing every method your API accepts, including `OPTIONS`. + +Do not use `*` for `Access-Control-Allow-Origin` with credentialed browser requests. Because this is a Lambda proxy +integration, the Lambda response must also include the CORS headers on actual requests. Configure gateway-generated +errors separately if your browser client must read their responses. + +CORS configuration page + +#### 1.5 Deploy the API Gateway + +Deploy the API to a stage named `dev` and record its invoke URL. AWS changes console labels periodically; verify the +resource, integration, `OPTIONS`, and gateway-response configuration in the deployed stage rather than relying only on +the screenshots in this guide. + +:::note[Update `apiDomain`, `apiBasePath`, and `apiGatewayPath` in both Lambda configuration and your frontend config if they have changed post API Gateway configuration.] +::: + +### 2. Set up Lambda layer + +#### 2.1 Create Lambda layer with required libraries + +Build the layer in the AWS SAM build image for the function's exact runtime and architecture. The commands below target +Lambda `x86_64` (`linux/amd64`). For a Lambda `arm64` function, change `PLATFORM` to `linux/arm64`. Do not build native +dependencies on an unrelated workstation OS or architecture. + +For Node.js, create `package.json` with exact direct dependency versions. Generate and review `package-lock.json` once, +commit it with the Lambda source, and build only with `npm ci`. The lock file pins the complete transitive graph; +`package.json` alone is not a deployment lock. + +For Python, create `requirements.in` with exact direct dependency versions. Compile and commit a hash-locked +`requirements.lock`, then install with `--require-hashes`. Do not deploy directly from `requirements.in`. + + + +```json title="package.json" +{ + "private": true, + "type": "module", + "dependencies": { + "@middy/core": "7.9.2", + "@middy/http-cors": "7.9.2", + "supertokens-node": "24.0.3" + } +} +``` + +```bash +PLATFORM=linux/amd64 +BUILD_IMAGE=public.ecr.aws/sam/build-nodejs24.x:1.165.0 + +# Run this only when intentionally updating the committed lock file. +docker run --rm --platform "$PLATFORM" \ + --volume "$PWD:/var/task" --workdir /var/task \ + "$BUILD_IMAGE" npm install --package-lock-only --ignore-scripts + +# Reproducible layer build from the reviewed lock file. +rm -rf node_modules nodejs supertokens-node.zip +docker run --rm --platform "$PLATFORM" \ + --volume "$PWD:/var/task" --workdir /var/task \ + "$BUILD_IMAGE" npm ci --omit=dev +mkdir nodejs +cp -R node_modules nodejs/ +zip -r supertokens-node.zip nodejs/ +``` + + +```text title="requirements.in" +fastapi==0.141.1 +mangum==0.22.0 +nest-asyncio==1.6.0 +supertokens-python==0.31.3 +``` + +```bash +PLATFORM=linux/amd64 +BUILD_IMAGE=public.ecr.aws/sam/build-python3.14:1.165.0 + +# Run this only when intentionally updating the committed lock file. +docker run --rm --platform "$PLATFORM" \ + --volume "$PWD:/var/task" --workdir /var/task \ + "$BUILD_IMAGE" sh -c \ + 'python -m pip install "pip-tools==7.6.1" && pip-compile --generate-hashes --output-file requirements.lock requirements.in' + +# Reproducible layer build from exact versions and package hashes. +rm -rf python supertokens-python.zip +docker run --rm --platform "$PLATFORM" \ + --volume "$PWD:/var/task" --workdir /var/task \ + "$BUILD_IMAGE" python -m pip install \ + --require-hashes --only-binary=:all: --target python --requirement requirements.lock +zip -r supertokens-python.zip python/ +``` + + + +For Node.js, pin the SAM image by digest in CI after verifying that the digest matches the selected platform. The version +tag above prevents implicit SAM CLI upgrades, while the digest prevents registry-tag movement. + +For Python, pin the SAM image by platform-specific digest in CI. Hash locking protects downloaded Python distributions; +the image digest protects the build tools and Amazon Linux environment. + +#### 2.2 Upload the SuperTokens Lambda layer + +Open AWS Lambda dashboard and click on layers: +AWS Lambda sidebar UI + +Click "Create Layer" button: +Create layer button UI + +Name the layer, upload the ZIP file, and select the same runtime family and architecture used for the container build. +These examples target the Amazon Linux 2023 Node.js 24 (`nodejs24.x`) and Python 3.14 (`python3.14`) runtime versions. +Test dependency lock updates before promotion. Monitor the [Lambda runtime support schedule](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) +and upgrade before deprecation. + + + +Lambda layer node configuration UI + + +Lambda layer python configuration UI + + + +### 3. Set up the Lambda function + +#### 3.1 Create a new Lambda function + + Click "Create Function" in the AWS Lambda dashboard, enter the function name and runtime, and create your Lambda function. + + +Create new Lambda configurations UI Node + + +Create new Lambda configurations UI Python + + + +#### 3.2 Link the Lambda layer with the Lambda function + +Scroll to the bottom and look for the `Layers` tab. Click on `Add a layer` +Link Lambda function with the Lambda layer + +Select `Custom Layer` and then select the layer created in step 2: + + + +Link custom layer with Lambda function Node + + +Link custom layer with Lambda function Python + + + +#### 3.3 Create a backend config file + +Using the editor provided by AWS, create a new config file and write the following code: + + + + +```javascript title="config.mjs" +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; + +export function getBackendConfig() { + return { + framework: "awsLambda", + supertokens: { + connectionURI: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + apiGatewayPath: "/dev", + }, + recipeList: [EmailPassword.init(), Session.init()], + isInServerlessEnv: true, + }; +} +``` + + +```python title="config.py" +from supertokens_python.recipe import emailpassword, session +from supertokens_python import SupertokensConfig, InputAppInfo + +supertokens_config = SupertokensConfig( + connection_uri="", +) + +app_info = InputAppInfo( + # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth", + api_gateway_path="/dev", +) + +framework = "fastapi" + +recipe_list = [ + session.init(), + emailpassword.init(), +] +``` + + + + +:::note[In the above code, notice the extra config of `apiGatewayPath` that was added to the `appInfo` object.] +The value of this should be whatever you have set as the value of your [AWS stage](https://docs.aws.amazon.com/apigateway/latest/developerguide/stages.html) which scopes your API endpoints. +For example, you may have a stage name for each environment: +- One for development (`/dev`). +- One for testing (`/test`). +- One for prod (`/prod`). + +So the value of `apiGatewayPath` should be set according to the above based on the environment it's running under. + +You also need to prepend the stage to `apiBasePath` in the frontend config. For example, when the frontend calls the +development stage and the backend `apiBasePath` is `/auth`, set the frontend value to `/dev/auth`. +::: + +:::note[You may edit the `apiBasePath` and `apiGatewayPath` values later if you have not set up API Gateway yet.] +::: + +#### 3.4 Add the SuperTokens auth middleware + +Using the editor provided by AWS, create/replace the handler file contents with the following code: + + + +```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/awsLambda"; +import { getBackendConfig } from "./config.mjs"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; + +supertokens.init(getBackendConfig()); + +export const handler = middy( + middleware((event) => { + // SuperTokens middleware didn't handle the route, return your custom response + return { + body: JSON.stringify({ + msg: "Hello!", + }), + statusCode: 200, + }; + }), +) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); +``` + + +```python title="handler.py" check=false reason="Requires surrounding application context" +import nest_asyncio +nest_asyncio.apply() + +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware +from mangum import Mangum + +from supertokens_python import init, get_all_cors_headers +from supertokens_python.framework.fastapi import get_middleware + +import config + +init( + supertokens_config=config.supertokens_config, + app_info=config.app_info, + framework=config.framework, + recipe_list=config.recipe_list, + mode="asgi", +) + +app = FastAPI(title="SuperTokens Example") + +app.add_middleware(get_middleware()) + +app = CORSMiddleware( + app=app, + allow_origins=[ + config.app_info.website_domain + ], + allow_credentials=True, + allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +handler = Mangum(app) +``` + + + + + +Add SuperTokens auth middleware UI + +The `.mjs` files use native ECMAScript modules. Supported Node.js Lambda runtimes load them without the deprecated +`--experimental-specifier-resolution=node` option. Keep explicit file extensions on relative imports. + + + +#### 3.5 Filter additional plugins or extensions (optional) + +If you are using AWS Lambda plugins, extensions, or anything that adds events to the lambda function (e.g. `serverless-plugin-warmup`), then you may need to prevent calling SuperTokens with them. + +These kinds of events lack request details that SuperTokens expects and might lead to unintended errors. + +Here's an example of how you can filter them out: + + +```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/awsLambda"; +import { getBackendConfig } from "./config.mjs"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; + +supertokens.init(getBackendConfig()); + +const httpHandler = middy( + middleware((event) => { + // SuperTokens middleware didn't handle the route, return your custom response + return { + body: JSON.stringify({ + msg: "Hello!", + }), + statusCode: 200, + }; + }), +) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); + +const postAuth = async (event, context) => { + // Plugins generally inject a `source` property in the event object. + if (event.source === "serverless-plugin-warmup") { + console.info("postAuth 010: warming up lambda. Bypassing authMiddleware."); + return { + statusCode: 200, + body: JSON.stringify({ message: "Warm-up successful" }), + }; + } + + return httpHandler(event, context); +}; + +export const handler = postAuth; +``` diff --git a/docs/integrations/aws-lambda/session-verification.mdx b/docs/integrations/aws-lambda/session-verification.mdx new file mode 100644 index 0000000000..a285d39e4e --- /dev/null +++ b/docs/integrations/aws-lambda/session-verification.mdx @@ -0,0 +1,587 @@ +--- +title: Session Verification +description: Verify user sessions when integrating SuperTokens with AWS Lambda. +sidebar: + order: 2 +--- + +The following page shows three ways to verify sessions in a Lambda integration. +Choose the one that works best based on the particularities of your use case. + +:::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] + +If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. +::: + + +## Using Session Verification + +When building your own APIs, you may need to verify the session of the user before proceeding further. +SuperTokens SDK exposes a `verifySession` function that can be utilized for this. +In this guide, we will be creating a `/user` `GET` route that will return the current session information. + +### 1. Add `/user` `GET` route in your API Gateway + +Create a `/user` resource and then `GET` method in your API Gateway. Configure the lambda integration and CORS just like we did [for the auth routes](/integrations/aws-lambda/quickstart-guide#13-attach-lambda-to-the-any-method-of-the-proxy-resource). + +### 2. Create a file in your Lambda function to handle the `/user` route + + + +An example of this is [here](https://github.com/supertokens/supertokens-node/blob/master/examples/aws/with-emailpassword/backend/user.mjs). + + + + + +```javascript title="user.mjs" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { getBackendConfig } from "./config.mjs"; +import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; + +supertokens.init(getBackendConfig()); + +const lambdaHandler = async (event) => { + return { + body: JSON.stringify({ + sessionHandle: event.session?.getHandle(), + userId: event.session?.getUserId(), + accessTokenPayload: event.session?.getAccessTokenPayload(), + }), + statusCode: 200, + }; +}; + +export const handler = middy(verifySession(lambdaHandler)) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); +``` + + +```python title="handler.py" check=false reason="Requires surrounding framework application context" +import nest_asyncio +nest_asyncio.apply() + +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware +from mangum import Mangum + +from supertokens_python import init, get_all_cors_headers +from supertokens_python.framework.fastapi import get_middleware + +import config + +init( + supertokens_config=config.supertokens_config, + app_info=config.app_info, + framework=config.framework, + recipe_list=config.recipe_list, + mode="asgi", +) + +app = FastAPI(title="SuperTokens Example") + +from fastapi import Depends +from supertokens_python.recipe.session.framework.fastapi import verify_session +from supertokens_python.recipe.session import SessionContainer + +@app.get("/user") +def user(s: SessionContainer = Depends(verify_session())): + return { + "sessionHandle": s.get_handle(), + "userId": s.get_user_id(), + "accessTokenPayload": s.get_access_token_payload() + } + +app.add_middleware(get_middleware()) + +app = CORSMiddleware( + app=app, + allow_origins=[ + config.app_info.website_domain + ], + allow_credentials=True, + allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +handler = Mangum(app) +``` + + + + + +Now, import this function in your `index.mjs` handler file as shown below: + + +:::note[The `verify_session` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `session_required=False` to the `verify_session` middleware.] + +If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `init` function and include the `session` recipe in the `recipe_list` for each respective lambda function. +::: + + + + + +```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/awsLambda"; +import { getBackendConfig } from "./config.mjs"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; +import { handler as userHandler } from "./user.mjs"; + +supertokens.init(getBackendConfig()); + +export const handler = middy( + middleware((event) => { + if (event.path === "/user") { + return userHandler(event); + } + + return { + body: JSON.stringify({ + msg: "Hello!", + }), + statusCode: 200, + }; + }), +) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); +``` + + + + + +:::note[The `verifySession` middleware automatically returns a 401 Unauthorized error if the session is not valid. You can alter the default behavior by passing `{ sessionRequired: false }` as the second argument to the `verifySession` middleware.] + +If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `supertokens.init` and include the `Session` recipe in the `recipeList` for each respective lambda function. +::: + + + +--- + +## Using Lambda Authorizers + + +You can use a Lambda authorizer with an API Gateway REST API to authorize requests to another integration, such as +AppSync. The authorizer below requires a valid session and returns its user ID as `principalId`. API Gateway can map +`$context.authorizer.principalId` to an integration header. Missing and invalid sessions are rejected; this guide does +not claim support for optional sessions because AWS's behavior for an empty principal is not established here. + +### 1. Add configurations and dependencies + +Refer to the [frontend](/quickstart#1-integrate-the-frontend-sdk), [lambda layer](/integrations/aws-lambda/quickstart-guide#2-set-up-lambda-layer), and [lambda setup](/integrations/aws-lambda/quickstart-guide#3-set-up-lambda). + +### 2. Add code to the lambda function handler + + + +Use the code below as the handler for the lambda. Remember that whenever we want to use any functions from the `supertokens-python` lib, we have to call the `init` function at the top of that serverless function file. We can then use `get_session()` to get the session. + + +Use the code below as the handler for the lambda. +Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. +We can then use `getSession()` to get the session. + + + + + +```python title="auth.py" check=false reason="Requires surrounding framework application context" +import nest_asyncio +import json +nest_asyncio.apply() + +from typing import Optional, Dict, Any + +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware +from mangum import Mangum + +from supertokens_python import init, get_all_cors_headers +from supertokens_python.framework.fastapi import get_middleware + +import config + +init( + supertokens_config=config.supertokens_config, + app_info=config.app_info, + framework=config.framework, + recipe_list=config.recipe_list, + mode="asgi", +) +app = FastAPI(title="SuperTokens Example") + +def generate_policy(principal_id: str, effect: str, resource: str, context: Optional[Dict[str, Any]]): + policy_document = { + "Version": "2012-10-17", + "Statement": [ + {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource} + ], + } + auth_response = { + "principalId": principal_id, + "policyDocument": policy_document, + "context": context or {}, + } + + return auth_response + + +def generate_allow(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): + return generate_policy(principal_id, "Allow", resource, context) + + +def generate_deny(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): + return generate_policy(principal_id, "Deny", resource, context) + + +from fastapi import Request +from supertokens_python.recipe.session.syncio import get_session +from supertokens_python.recipe.session.exceptions import (InvalidClaimsError, + TryRefreshTokenError, + UnauthorisedError) + +@app.get("/{full_path:path}") +def handle_auth(request: Request, full_path: str): + event = request.scope["aws.event"] + method_arn = event.get("methodArn") + + try: + session = get_session(request) + return generate_allow(session.get_user_id(), method_arn) + except Exception as e: + if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): + raise Exception("Unauthorized") + if isinstance(e, InvalidClaimsError): + claim_validation_errors = [err.to_json() for err in e.payload] + return generate_deny( + "invalid-claims", + method_arn, + { + "body": json.dumps({ + "message": "invalid claims", + "claimValidationErrors": claim_validation_errors, + }) + }, + ) + + raise e + + +app.add_middleware(get_middleware()) + +app = CORSMiddleware( + app=app, + allow_origins=[ + config.app_info.website_domain + ], + allow_credentials=True, + allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +def handler(event: Dict[str, Any], context: Any): + mangum_handler = Mangum(app) + response: Dict[str, Any] = mangum_handler(event, context) + + if event.get("methodArn"): + return json.loads(response["body"]) + + return response +``` + + +```javascript title="index.mjs" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; + +import { getBackendConfig } from "./config.mjs"; + +supertokens.init(getBackendConfig()); + +export const handler = async function (event) { + try { + const session = await Session.getSession(event, event); + return generateAllow(session.getUserId(), event.methodArn); + } catch (ex) { + if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") { + throw new Error("Unauthorized"); + } + if (ex.type === "INVALID_CLAIMS") { + return generateDeny("invalid-claims", event.methodArn, { + body: JSON.stringify({ + message: "invalid claim", + claimValidationErrors: ex.payload, + }), + }); + } + throw ex; + } +}; + +const generatePolicy = function (principalId, effect, resource, context = {}) { + const policyDocument = { + Version: "2012-10-17", + Statement: [], + }; + + const statementOne = { + Action: "execute-api:Invoke", + Effect: effect, + Resource: resource, + }; + + policyDocument.Statement[0] = statementOne; + + const authResponse = { + principalId: principalId, + policyDocument: policyDocument, + context, + }; + + return authResponse; +}; + +const generateAllow = function (principalId, resource, context) { + return generatePolicy(principalId, "Allow", resource, context); +}; + +const generateDeny = function (principalId, resource, context) { + return generatePolicy(principalId, "Deny", resource, context); +}; +``` + + + +The authorizer `context` map may contain only scalar values. The invalid-claims body is therefore JSON-serialized in +both examples. Do not join multiple `Set-Cookie` values into one authorizer context string: commas are valid inside +cookie attributes and API Gateway may not reconstruct the original headers. Return auth-route cookies from the Lambda +proxy response as distinct values: REST API payload format 1.0 uses +`multiValueHeaders: { "Set-Cookie": cookies }`, while HTTP API payload format 2.0 uses the top-level `cookies` array. +Before relying on cookie mutation from an authorizer, an E2E fixture must prove no-cookie, one-cookie, multiple-cookie, +refresh, denied, and gateway-error paths for the exact REST/HTTP API payload version in use. + +### 3. Configure the authorizer + +Create a request-based Lambda authorizer for the REST API and point it to the function above. AWS changes console labels; +capture this configuration in IaC and verify that the deployed authorizer receives the headers and cookies required by +your selected SuperTokens token-transfer method. + +### 4. Configure API Gateway + + +- Require the authorizer on each protected method. +- In the integration request, overwrite `x-user-id` from `context.authorizer.principalId`. Never forward a + client-supplied identity header. +- If the browser must read gateway-generated `401` or `403` responses, configure them with the exact trusted + `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials: true`. Do not combine credentials with a wildcard + origin. +- Deploy and test the API. The IaC fixture must prove that a spoofed identity header cannot reach the integration. + +--- + +## Using JWT Authorizers + +:::warning + +AWS supports JWT authorizers for HTTP APIs and not REST APIs on the API Gateway service. For REST APIs follow the [Lambda authorizer](/integrations/aws-lambda/session-verification#using-lambda-authorizers) guide + +This guide will work if you are using **SuperTokens Session Tokens**. + +If you are implementing an **OAuth2** setup, through the [**Unified Login**](/authentication/unified-login/introduction) or the [**Microservice Authentication**](/authentication/m2m/client-credentials) features, you will have to manually set the token audience property. +Please check the referenced pages for more information. + +::: + +### 1. Add the `aud` claim in the JWT based on the authorizer configuration + + + + +```javascript title="config.mjs" +import Session from "supertokens-node/recipe/session"; + +export function getBackendConfig() { + return { + framework: "awsLambda", + supertokens: { + connectionURI: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + apiGatewayPath: "/dev", + }, + recipeList: [ + Session.init({ + exposeAccessTokenToFrontendInCookieBasedAuth: true, + override: { + functions: function (originalImplementation) { + return { + ...originalImplementation, + createNewSession: async function (input) { + input.accessTokenPayload = { + ...input.accessTokenPayload, + /* + * AWS requires JWTs to contain an audience (aud) claim + * The value for this claim should be the same + * as the value you set when creating the + * authorizer + */ + aud: "jwtAuthorizers", + }; + + return originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], + isInServerlessEnv: true, + }; +} +``` + + +```python title="config.py" +from supertokens_python.recipe import session +from supertokens_python import ( + InputAppInfo, + SupertokensConfig, +) +from supertokens_python.recipe.session.interfaces import RecipeInterface as SessionRecipeInterface + +from typing import Any, Dict, Optional +from supertokens_python.types import RecipeUserId + +supertokens_config = SupertokensConfig( + connection_uri="", +) + +app_info = InputAppInfo( + # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth", + api_gateway_path="/dev", +) + +framework = "fastapi" + +def override_session_functions(oi: SessionRecipeInterface) -> SessionRecipeInterface: + oi_create_new_session = oi.create_new_session + + async def create_new_session( + user_id: str, + recipe_user_id: RecipeUserId, + access_token_payload: Optional[Dict[str, Any]], + session_data_in_database: Optional[Dict[str, Any]], + disable_anti_csrf: Optional[bool], + tenant_id: str, + user_context: Dict[str, Any], + ): + # AWS requires JWTs to contain an audience (aud) claim + # The value for this claim should be the same as the + # value you set when creating the authorizer + + if access_token_payload is None: + access_token_payload = {} + + access_token_payload["aud"] = "jwtAuthorizers" + return await oi_create_new_session(user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) + + oi.create_new_session = create_new_session + return oi + +recipe_list = [ + session.init( + override=session.InputOverrideConfig( + functions=override_session_functions, + ), + expose_access_token_to_frontend_in_cookie_based_auth=True, + ), +] +``` + + + + +### 2. Configure your authorizer + +- Go to the "Authorizers" tab in the API Gateway configuration and select the "Manage authorizers" tab +- Click "Create", in the creation screen select "JWT" as the "Authorizer type" +- Enter a name for your authorizer (You can enter any name for this field) +- Use `$request.header.Authorization` for the "Identity source". This means that API requests will contain the JWT as a Bearer token under the request header "Authorization". +- Use the exact normalized issuer emitted by SuperTokens for this configuration: `/dev/auth`. This is `apiDomain + apiGatewayPath + apiBasePath`, with one slash at each boundary. +- Set a value for the "Audience" field, this will be the value you expect the JWT to have under the `aud` claim. In the backend config above the value is set to `"jwtAuthorizers"` + +### 3. Add the authorizer to your API +- In the "Authorization" section select the "Attach authorizers to routes" tab +- Click on the route you want to add the authorizer to and select the authorizer you created from the dropdown +- Click "Attach authorizer" +- Deploy your changes and test your API + +### 4. Send the access token as a bearer token + +Exposing the access token does not automatically copy it to the JWT authorizer's identity source in cookie-based auth. +Set the header explicitly on requests to protected routes: + +```javascript +import Session from "supertokens-web-js/recipe/session"; + +async function fetchProtectedUser() { + const accessToken = await Session.getAccessToken(); + + if (accessToken === undefined) { + throw new Error("No session access token is available"); + } + + return fetch("/dev/user", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); +} +``` + +Keep the SuperTokens frontend SDK's network interception enabled so session refresh continues to work. Test the +expired-token retry path against the deployed HTTP API. + +### 5. Check authorization claims in the JWT + +Once the JWT authorizer successfully validates the JWT, the claims of the JWT will be available to your lambda functions via `$event.requestContext.authorizer.jwt.claims`. You should check for the right authorization access here. +For example, if one of your lambda functions requires that the user's email is verified, then it should check for the `jwt` payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. Similar checks need to be done to enforce the right user role or if 2FA is completed or not. +This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in this flow, you will have to check them on your own. diff --git a/docs/integrations/graphql.mdx b/docs/integrations/graphql.mdx new file mode 100644 index 0000000000..6cc6d9a863 --- /dev/null +++ b/docs/integrations/graphql.mdx @@ -0,0 +1,187 @@ +--- +title: GraphQL +description: Set up backend session verification in GraphQL resolvers using SuperTokens for user authentication. +sidebar: + icon: /docs-assets/img/logos/graphql.svg + order: 30 +--- + +## Before you start + +These instructions only show you how to perform **session verification** in a GraphQL context. +You will first have to go through the [quickstart guide](/quickstart#1-integrate-the-frontend-sdk) to configure **SuperTokens** and then return to this page. + +:::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] + +If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. +::: + +## Using the GraphQL context + +We want to use the `Session.getSession` function in the `context` function to verify the session, and add the `userId` into our context so that our resolvers can read it. +If the user id not logged in, we will set the `userId` to `undefined` in the context + +```tsx +import { ApolloServer } from "@apollo/server"; +import express from "express"; +import { expressMiddleware } from "@apollo/server/express4"; +import { GraphQLError } from "graphql"; +import Session from "supertokens-node/recipe/session"; + +let app = express(); + +const typeDefs = "..."; +const resolvers = { + /* ... */ +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +server.start().then(() => { + app.use( + express.json(), + expressMiddleware(server, { + // Note: This example uses the `req` and `res` argument to access headers, + // but the arguments received by `context` vary by integration. + // This means they vary for Express, Fastify, Lambda, etc. + context: async ({ req, res }) => { + try { + let session = await Session.getSession(req, res, { + sessionRequired: false, + }); + return { + userId: session !== undefined ? session.getUserId() : undefined, + }; + } catch (err) { + if (Session.Error.isErrorFromSuperTokens(err)) { + throw new GraphQLError("Session related error", { + extensions: { + code: "UNAUTHENTICATED", + http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, + }, + }); + } + throw err; + } + }, + }), + ); + + app.listen(3001, () => { + console.log("Server started"); + }); +}); +``` + +In the above code snippet, we first attempt to verify the session using the `Session.getSession` function. +If the session is valid, we will add the `userId` to the context. +If the access token has expired, we will throw an error with a status code of `401`. +If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. + +The `401` status code will cause the session refresh flow to start, which will give a new access token to the user, or else if the session was revoked, the user will be logged out. + +In case the user is not logged in, the `Session.getSession` function will throw return `undefined`, in which case, your resolvers will not have a `userId` in the context. + +The downside of this method is that if you want to mutate the session's access token payload in one of your resolvers, then you don't have access to the `session` object in there. This is where the method below comes into the picture: + +## Using the GraphQL resolver + +Unlike the method above, we will be doing session verification on a per resolver basis here. +This means that you will have access to the `session` object in your resolver using which you can update the information in the session (like its access token payload). + +We start by creating a helper function (a sort of middleware for your resolver) which you will have to call in all of your resolvers that require a session: + +```tsx +import Session, { SessionContainer } from "supertokens-node/recipe/session"; +import { GraphQLError } from "graphql"; + +async function withSession(contextValue: any, resolver: (session: SessionContainer) => Promise) { + try { + let session = await Session.getSession(contextValue.req, contextValue.res); + return await resolver(session); + } catch (err) { + if (Session.Error.isErrorFromSuperTokens(err)) { + throw new GraphQLError("Session related error", { + extensions: { + code: "UNAUTHENTICATED", + http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, + }, + }); + } + } +} +``` + +In the above function, we attempt to verify the session using `Session.getSession`. If the session is valid, we will call the `resolver` function with the `session` object. If the access token has expired, or if the session does not exist, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. + +For this resolver to work, we will have to add the `req` and `res` object into the GraphQL context. This can be done as follows: + +```tsx +import { ApolloServer } from "@apollo/server"; +import express from "express"; +import { expressMiddleware } from "@apollo/server/express4"; +import { GraphQLError } from "graphql"; + +let app = express(); + +const typeDefs = "..."; +const resolvers = { + /* ... */ +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +server.start().then(() => { + app.use( + express.json(), + expressMiddleware(server, { + // Note: This example uses the `req` and `res` argument to access headers, + // but the arguments received by `context` vary by integration. + // This means they vary for Express, Fastify, Lambda, etc. + context: async ({ req, res }) => { + return { + req, + res, + }; + }, + }), + ); + + app.listen(3001, () => { + console.log("Server started"); + }); +}); +``` + +Finally, we can use our `withSession` in our resolvers as shown below: + +```tsx check=false reason="Requires surrounding application context" +import { ApolloServer } from "@apollo/server"; + +const server = new ApolloServer({ + typeDefs, + resolvers: { + Query: { + userProfile: async (_: any, __: any, contextValue) => { + // starts of your resolver code.. + return await withSession(contextValue, async (session) => { + // getUserName is a custom application function... + let name = await getUserName(session.getUserId()); + + return { + userId: session.getUserId(), + sessionHandle: session.getHandle(), + name, + }; + }); + }, + }, + }, +}); +``` diff --git a/docs/integrations/hasura.mdx b/docs/integrations/hasura.mdx new file mode 100644 index 0000000000..ffcffbb581 --- /dev/null +++ b/docs/integrations/hasura.mdx @@ -0,0 +1,389 @@ +--- +title: Hasura +description: Learn how to use SuperTokens with Hasura +sidebar: + icon: /docs-assets/img/logos/hasura.svg + order: 40 +--- + +## Before you start + +The tutorial assumes that you already have a working application integrated with **SuperTokens**. +If you have not, please check the [Quickstart Guide](/quickstart). + +Using SuperTokens with Hasura requires you to host your own API layer that uses our Backend SDK. +If you do not want to host your own server you can use a serverless environment to achieve this. + +:::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] + +If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. +::: + + +## Steps + +### 1. Expose the access token to the frontend + +For cookie based auth, the access token is not available on the frontend by default. +In order to expose it, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` config to `true`. + + + +```tsx +import SuperTokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + exposeAccessTokenToFrontendInCookieBasedAuth: true, + }), + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + session.Init(&sessmodels.TypeInput{ + ExposeAccessTokenToFrontendInCookieBasedAuth: true, + }), + }, + }) +} +``` + + +```python check=false reason="Partial configuration example" +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import session + +init( + app_info=InputAppInfo( + api_domain="...", app_name="...", website_domain="..."), + framework='...', + recipe_list=[ + session.init( + expose_access_token_to_frontend_in_cookie_based_auth=True, + ) + ] +) +``` + + + +### 2. Add custom claims to the JWT + +Hasura requires claims to be set in a specific way, read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html) to know more. + + + +```tsx +import SuperTokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; + +SuperTokens.init({ + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + exposeAccessTokenToFrontendInCookieBasedAuth: true, + override: { + functions: function (originalImplementation) { + return { + ...originalImplementation, + createNewSession: async function (input) { + input.accessTokenPayload = { + ...input.accessTokenPayload, + "https://hasura.io/jwt/claims": { + "x-hasura-user-id": input.userId, + "x-hasura-default-role": "user", + "x-hasura-allowed-roles": ["user"], + }, + }; + + return originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], +}); +``` + + +```go +import ( + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + supertokens.Init(supertokens.TypeInput{ + RecipeList: []supertokens.Recipe{ + session.Init(&sessmodels.TypeInput{ + ExposeAccessTokenToFrontendInCookieBasedAuth: true, + Override: &sessmodels.OverrideStruct{ + Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { + + originalCreateNewSession := *originalImplementation.CreateNewSession + + (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload map[string]interface{}, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { + if accessTokenPayload == nil { + accessTokenPayload = map[string]interface{}{} + } + + hasuraClaims := map[string]interface{}{ + "x-hasura-user-id": userID, + "x-hasura-default-role": "user", + "x-hasura-allowed-roles": []string{"user"}, + } + + accessTokenPayload["https://hasura.io/jwt/claims"] = hasuraClaims + + return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) + } + + return originalImplementation + }, + }, + }), + }, + }) +} +``` + + +```python check=false reason="Partial configuration example" +from supertokens_python import init, InputAppInfo +from supertokens_python.recipe import session +from supertokens_python.recipe.session.interfaces import RecipeInterface +from typing import Dict, Optional, Any +from supertokens_python.types import RecipeUserId + + +def override_functions(original_implementation: RecipeInterface): + original_implementation_create_new_session = ( + original_implementation.create_new_session + ) + + async def create_new_session( + user_id: str, + recipe_user_id: RecipeUserId, + access_token_payload: Optional[Dict[str, Any]], + session_data_in_database: Optional[Dict[str, Any]], + disable_anti_csrf: Optional[bool], + tenant_id: str, + user_context: Dict[str, Any], + ): + + if access_token_payload is None: + access_token_payload = {} + + access_token_payload["https://hasura.io/jwt/claims"] = { + "x-hasura-user-id": user_id, + "x-hasura-default-role": "user", + "x-hasura-allowed-roles": ["user"], + } + + return await original_implementation_create_new_session( + user_id, + recipe_user_id, + access_token_payload, + session_data_in_database, + disable_anti_csrf, + tenant_id, + user_context, + ) + + original_implementation.create_new_session = create_new_session + return original_implementation + + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + framework="...", + recipe_list=[ + session.init( + override=session.InputOverrideConfig(functions=override_functions), + expose_access_token_to_frontend_in_cookie_based_auth=True, + ) + ], +) +``` + + + +### 3. Configure Hasura environment variables + +:::info +Read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html#configuring-jwt-mode) to know about setting the JWT secret environment variable on Hasura +::: + +To use JWT based authentication, Hasura requires setting environment variables when configuring your app. +With SuperTokens this can be done in 2 ways: + +#### Using the JWKS endpoint + +When configuring Hasura, you can set the `jwk_url` property. + +```json +{ + "jwk_url": "//auth/jwt/jwks.json" +} +``` + +You can get the JWKS URL for your backend by using the method explained [here](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library) + +#### Using a key string + +Hasura let's you provide a PEM string in the configuration. +Refer to [this page](/additional-verification/session-verification/protect-api-routes#with-the-public-key-string) to learn how to get a public key as a string. You can then use that key string in the Hasura config: + +```json +{ + "type": "RS256", + "key": "CERTIFICATE_STRING" +} +``` + +### 4. Check for claim values in Hasura + +Some checks like if the email is verified, or if 2FA is completed are stored as claim values in the JWT. +You should check for the values of these claims in your GraphQL functions wherever required. +For example, if one of your GraphQL functions requires that the user's email is verified, then it should check for the JWT payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. + +You can also use a [custom Hasura authorizer webhook](https://hasura.io/docs/latest/auth/authentication/webhook/) to check for the values of these claims depending on your app's requirements. + +This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. +Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in the Hasura flow, you will have to check them on your own. + +### 5. Make requests to Hasura + +#### 5.1 Get the JWT on the frontend + + + + + + + + + +```tsx +import Session from "supertokens-web-js/recipe/session"; + +async function getToken(): Promise { + const accessToken = await Session.getAccessToken(); + console.log(accessToken); +} +``` + + +```tsx check=false reason="Requires SDK globals from surrounding application" +async function getToken(): Promise { + const accessToken = await supertokensSession.getAccessToken(); + console.log(accessToken); +} +``` + + + + + + +```tsx +import SuperTokens from "supertokens-react-native"; + +async function getToken(): Promise { + const accessToken = await SuperTokens.getAccessToken(); + console.log(accessToken); +} +``` + + +```kotlin +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + fun getToken(): String { + return SuperTokens.getAccessToken(applicationContext) + } +} +``` + + +```swift +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func getToken() -> String? { + return SuperTokens.getAccessToken() + } +} +``` + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; + +Future getToken() async { + return await SuperTokens.getAccessToken(); +} +``` + + + + + + + +#### 5.2 Make an HTTP requests + +```tsx +import axios from "axios"; + +async function makeRequest() { + let url = "..."; + let jwt = "..."; // Refer to step 5.a + let response = await axios.get(url, { + headers: { + Authorization: `Bearer ${jwt}`, + }, + }); +} +``` + +## Local development + + +If you are using Hasura cloud and testing your backend APIs in your local environment, JWT verification will fail because Hasura will not be able to query the JWKS endpoint (because the cloud can not query your local environment i.e localhost, 127.0.0.1). + +To solve this problem you will need to expose your locally hosted backend APIs to the internet. For example you can use [ngrok](https://ngrok.com/). +After that, you need to configure Hasura to use the `//auth/jwt/jwks.json` as the JWKS endpoint (explained in [step 3](#3-configure-hasura-environment-variables)). diff --git a/docs/integrations/meta.ts b/docs/integrations/meta.ts new file mode 100644 index 0000000000..3dc1e562d1 --- /dev/null +++ b/docs/integrations/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Integrations", + icon: "puzzle", + order: 65, + collapsed: true, +}); diff --git a/docs/integrations/nestjs.mdx b/docs/integrations/nestjs.mdx new file mode 100644 index 0000000000..a378f98569 --- /dev/null +++ b/docs/integrations/nestjs.mdx @@ -0,0 +1,219 @@ +--- +title: NestJS +description: Integrate SuperTokens with NestJS backend, covering modules, services, middleware, and session management. +sidebar: + icon: /docs-assets/img/logos/nestjs.svg + order: 50 +--- + +## Overview + + +Integrating SuperTokens into a NestJS backend differs in some aspects from the main quickstart guide. +That's because of the additional framework specific entities that are involved. +To aid the process you can use the `supertokens-nestjs` package which exposes abstractions that speed up the setup. + +## Before you start + +This guide assumes that you have already completed the [main quickstart guide](/quickstart). +If not, please go through it before continuing with this page. +You need to first understand how to configure the required recipes and run a sample project. + +You can also explore the [example projects](https://github.com/supertokens/supertokens-nestjs/tree/main/examples) for complete code references on how to use the libraries. + +## Steps + +### 1. Install the required packages + + +```bash title="npm" +npm i -s supertokens-node supertokens-nestjs +``` + +```bash title="Yarn" +yarn add supertokens-node supertokens-nestjs +``` + +```bash title="pnpm" +pnpm add supertokens-node supertokens-nestjs +``` + +```bash title="Bun" +bun add supertokens-node supertokens-nestjs +``` + + +### 2. Initialize the `SuperTokensModule` + +Inside your main application module, initialize the **SuperTokensModule** with your required configuration. + +```tsx +import { Module } from "@nestjs/common"; +import { SuperTokensModule } from "supertokens-nestjs"; + +@Module({ + imports: [ + SuperTokensModule.forRoot({ + // Choose between 'express' and 'fastify' + // If you are using fastify make sure to also set the fastifyAdapter property + framework: "express", + supertokens: { + connectionURI: "...", + }, + appInfo: { + appName: "...", + apiDomain: "...", + websiteDomain: "...", + }, + recipeList: [ + /* ... */ + ], + }), + ], + controllers: [ + /* ... */ + ], + providers: [ + /* ... */ + ], +}) +export class AppModule {} +``` + + +:::info[Tip] +You can use the `SuperTokensModule.forRootAsync` if you want to load the configuration asynchronously. +::: + +### 3. Update the `bootstrap` function + +Inside your `bootstrap` function, you have to update the CORS configuration and set the exception filter. +**SuperTokens** generates a set of CORS headers that the authentication flow requires. +And, the global filter ensures that all authentication related errors get handled by the SDK. + +```tsx check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { SuperTokensExceptionFilter } from "supertokens-nestjs"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { appInfo } from "./config"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: [appInfo.websiteDomain], + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }); + app.useGlobalFilters(new SuperTokensExceptionFilter()); + + await app.listen(3001); +} +``` +### 4. Add the `SuperTokensAuthGuard` + +The `SuperTokensAuthGuard` automatically marks the routes that it targets as protected. +By default session validation gets performed based on the default configuration provided in the `Session.init` call. +You can customize the validation logic with decorators. +More on that in the next step. + +#### As a global guard + +This applies the `SuperTokensAuthGuard` to all routes in exposed by controllers registered in that module. + +```tsx +import { Module } from "@nestjs/common"; +import { APP_GUARD } from "@nestjs/core"; +import { SuperTokensAuthGuard } from "supertokens-nestjs"; + +@Module({ + imports: [ + /* ... */ + ], + controllers: [ + /* ... */ + ], + providers: [ + { + provide: APP_GUARD, + useClass: SuperTokensAuthGuard, + }, + ], +}) +export class AppModule {} +``` + +#### As a controller guard + +This applies the `SuperTokensAuthGuard` only to the routes defined in the controller. + +```tsx +import { Controller, UseGuards } from "@nestjs/common"; +import { SuperTokensAuthGuard } from "supertokens-nestjs"; + +@Controller() +@UseGuards(SuperTokensAuthGuard) +export class AppController {} +``` + +### 5. Manage authentication with decorators + +The `supertokens-nestjs` package exposes two sets of decorators: +- Function decorators like `VerifySession` and `PublicAccess` that you can use on controller methods to customize the session validation logic. +- Parameter decorators like `Session` that you can use to access the session data in your controller methods. + + +```tsx +import { Controller, Delete, Get, Patch, Post } from "@nestjs/common"; +import { PublicAccess, Session, VerifySession } from "supertokens-nestjs"; +import { SessionContainer } from "supertokens-node/recipe/session"; + +@Controller() +class AppController { + @Get("/user") + @VerifySession() + async getUserInfo(@Session("userId") userId: string) {} + + @Get("/user/:userId") + @VerifySession({ + roles: ["admin"], + }) + async deleteUser(@Session() session: SessionContainer) {} + + @Get("/user/profile") + @PublicAccess() + async getUserProfile() {} +} +``` + +:::info[tip] +With the `VerifySession` decorator, you can specify the following options: +| Option | Type | Description | +|--------|------|-------------| +| `roles` | `string[]` | Roles that the user must have to access the route | +| `permissions` | `string[]` | Permissions that the user must have to access the route | +| `requiresMfa` | `boolean` | Indicates whether the user must have MFA enabled to access the route | +| `requireEmailVerification` | `boolean` | Indicates whether the user must have their email verified to access the route | +| `options` | `VerifySessionOptions` | The value that normally passed to the `getSession` or `verifySession` functions. Use it if you want additional levels of customization. | + +::: + + +### 6. Configure SuperTokens core + + + + +You need to setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: +- [Managed service](/quickstart#3-configure-the-core-service) +- [Self hosted](/deployment/self-host-supertokens) + + + + + +:::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] +::: + + + diff --git a/docs/integrations/netlify.mdx b/docs/integrations/netlify.mdx new file mode 100644 index 0000000000..7a6a7a0825 --- /dev/null +++ b/docs/integrations/netlify.mdx @@ -0,0 +1,199 @@ +--- +title: Netlify +description: Learn to set up SuperTokens authentication with Netlify using serverless functions. +sidebar: + icon: /docs-assets/img/logos/netlify.svg + order: 60 +--- + +## Overview + +The following guide gets you though how to add SuperTokens to a Netlify serverless API. +You can also check out the [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-netlify) for a full working example. + +## Before you start + +This guide assumes that you are using Netlify for hosting your serverless API functions. +If this is not the case, and you are only hosting your frontend using Netlify, please follow the [Quick setup guide](/quickstart#1-integrate-the-frontend-sdk) instead. + +## Steps + +### 1. Setup the frontend + +Follow the [initial quickstart guide](/quickstart#1-integrate-the-frontend-sdk) to configure the frontend. + +### 2. Setup the backend + +#### 2.1 Install the SuperTokens node package + + +```bash title="npm" +npm i supertokens-node +``` + +```bash title="Yarn" +yarn add supertokens-node +``` + +```bash title="pnpm" +pnpm add supertokens-node +``` + +```bash title="Bun" +bun add supertokens-node +``` + + +#### 2.2 Create a configuration file + +Create a `config` folder in the root directory of your project. +Create a `supertokensConfig.js` inside the `config` folder. +An example of this file can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/config/supertokensConfig.js). + +#### 2.3 Create a backend configuration function + + +```tsx title="/config/supertokensConfig.ts" +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; + +function getBackendConfig() { + return { + framework: "awsLambda", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [EmailPassword.init(), Session.init()], + isInServerlessEnv: true, + }; +} + +module.exports.getBackendConfig = getBackendConfig; +``` + + +### 3. Expose the authentication APIs + +We will add all the backend APIs for auth on `/.netlify/functions/auth/*`. +This can be changed by setting the `apiBasePath` property in the `appInfo` object on the backend and frontend. +For the rest of this page, we will assume you are using `/.netlify/functions/auth/*`. + +#### 3.1 Create the `netlify/functions/auth.js` page + +Be sure to create the `netlify/functions/` folder. +An example of this can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/auth.js). + +```tsx title="netlify/functions/auth.ts" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/awsLambda"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; +import { getBackendConfig } from "../../config/supertokensConfig"; + +supertokens.init(getBackendConfig()); + +module.exports.handler = middy( + middleware(async (event, context) => { + if (event.httpMethod === "OPTIONS") { + return { + statusCode: 200, + body: "", + }; + } + + return { + statusCode: 404, + body: "Not Found", + }; + }), +) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); +``` + +:::note[- Notice that we called `supertokens.init` above. We will need to call this in all API endpoints that use any functions related to SuperTokens.] +- `CORS` is only needed if you are hosting your frontend using a separate domain (if your website domain is different that your API's domain). +::: + +#### 3.2 Use the login widget +If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) + +### 4. Add session verification + +:::warning[This guide only applies to scenarios which involve **SuperTokens Session Access Tokens**.] + +If you are implementing either, [**Unified Login**](/authentication/unified-login/introduction) or [**Microservice Authentication**](/authentication/m2m/introduction), features that make use of **OAuth2 Access Tokens**, please check the [separate page](/authentication/unified-login/verify-tokens) that shows you how to verify those types of tokens. +::: + +For this guide, we will assume that we want an API `/.netlify/functions/user GET` which returns the current session information. + +#### 4.1 Create a new file `netlify/functions/user.js` + +An example of this is [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/user.js). + +#### 4.2 Call the `supertokens.init` function + +Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. + +```tsx title="netlify/functions/user.ts" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { getBackendConfig } from "../../config/supertokensConfig"; + +supertokens.init(getBackendConfig()); +``` + +#### 4.3 Use session verification with your API handler + +We use the `verifySession()` middleware to verify a session. + +```tsx title="netlify/functions/user.ts" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; +import { SessionEvent } from "supertokens-node/framework/awsLambda"; +import middy from "@middy/core"; +import cors from "@middy/http-cors"; +import { getBackendConfig } from "../../config/supertokensConfig"; + +supertokens.init(getBackendConfig()); + +const handler = async (event: SessionEvent) => { + return { + body: JSON.stringify({ + sessionHandle: event.session!.getHandle(), + userId: event.session!.getUserId(), + accessTokenPayload: event.session!.getAccessTokenPayload(), + }), + }; +}; + +module.exports.handler = middy(verifySession(handler)) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { + throw request.error; + }); +``` diff --git a/docs/integrations/nextjs/app-directory/about.mdx b/docs/integrations/nextjs/app-directory/about.mdx new file mode 100644 index 0000000000..d7cc3cde02 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/about.mdx @@ -0,0 +1,58 @@ +--- +title: About +description: Integrate SuperTokens with Next.js for authentication, session verification, and route protection. +sidebar: + order: 1 +--- + + + + + + +Integrating SuperTokens with a Next.js app involves: +- Calling the frontend and backend init functions +- Adding a website page to display the auth related widgets (on `/auth` by default) +- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) +- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page +- Performing session verification: + - In your APIs + - In your frontend routes + +## Try an example app +Download and run an example Next.js app quickly using the following command: + +```bash +npx create-supertokens-app@latest --frontend=next --recipe=emailpassword +``` + +## Follow our video guide + + + + + + + + +Integrating SuperTokens with a Next.js app involves: +- Calling the frontend and backend init functions +- Building the various auth flows as per the [custom UI setup guide](/quickstart#1-integrate-the-frontend-sdk). +- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) +- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page +- Performing session verification: + - In your APIs + - In your frontend routes + +## Try an example app + +Download and run an example Next.js app quickly using the following command: + +```bash +npx create-supertokens-app@latest --frontend=next --recipe=emailpassword +``` + +:::note[This example app uses our pre-built UI] +::: + + diff --git a/docs/integrations/nextjs/app-directory/init.mdx b/docs/integrations/nextjs/app-directory/init.mdx new file mode 100644 index 0000000000..815d56328b --- /dev/null +++ b/docs/integrations/nextjs/app-directory/init.mdx @@ -0,0 +1,297 @@ +--- +title: 1. Configuration +description: Configure SuperTokens for authentication in your Next.js app with frontend and backend setup. +sidebar: + order: 2 +--- + + + + + +## 1. Install `supertokens` package + +```bash title="npm" +npm install supertokens-node supertokens-auth-react supertokens-web-js +``` + +```bash title="Yarn" +yarn add supertokens-node supertokens-auth-react supertokens-web-js +``` + +```bash title="pnpm" +pnpm add supertokens-node supertokens-auth-react supertokens-web-js +``` + +```bash title="Bun" +bun add supertokens-node supertokens-auth-react supertokens-web-js +``` + + +## 2. Create configuration files +- Create a `config` folder in the app directory of your project. +- Create an `appInfo.ts` inside the `config` folder. +- Create a `backend.ts` inside the `config` folder. +- Create a `frontend.ts` inside the `config` folder. + +## 3. Create the `appInfo` configuration. + + +```tsx title="app/config/appInfo.ts" +export const appInfo = { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", +}; +``` + + + + + +## 1. Install `supertokens` package + +```bash title="npm" +npm install supertokens-node supertokens-web-js +``` + +```bash title="Yarn" +yarn add supertokens-node supertokens-web-js +``` + +```bash title="pnpm" +pnpm add supertokens-node supertokens-web-js +``` + +```bash title="Bun" +bun add supertokens-node supertokens-web-js +``` + + +## 2. Create configuration files +- Create a `config` folder in the app directory of your project +- Create an `appInfo.ts` inside the `config` folder. +- Create a `backend.ts` inside the `config` folder. +- Create a `frontend.ts` inside the `config` folder. + +## 3. Create the `appInfo` configuration. + + +```tsx title="app/config/appInfo.ts" +export const appInfo = { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + websiteDomain: "", + apiDomain: "", + apiBasePath: "/auth", +}; +``` + + + + + + +## 4. Create a frontend config function + +```tsx title="app/config/frontend.tsx" check=false reason="Requires surrounding framework application context" +import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword"; +import SessionReact from "supertokens-auth-react/recipe/session"; +import { appInfo } from "./appInfo"; +import { useRouter } from "next/navigation"; +import type { SuperTokensConfig } from "supertokens-auth-react/lib/build/types"; + +const routerInfo: { router?: ReturnType; pathName?: string } = {}; + +export function setRouter(router: ReturnType, pathName: string) { + routerInfo.router = router; + routerInfo.pathName = pathName; +} + +export const frontendConfig = (): SuperTokensConfig => { + return { + appInfo, + recipeList: [EmailPasswordReact.init(), SessionReact.init()], + windowHandler: (original) => ({ + ...original, + location: { + ...original.location, + getPathName: () => routerInfo.pathName!, + assign: (url) => routerInfo.router!.push(url.toString()), + setHref: (url) => routerInfo.router!.push(url.toString()), + }, + }), + }; +}; +``` + + + + + +## 4. Create a frontend config function + +```tsx title="app/config/frontend.tsx" check=false reason="Requires surrounding framework application context" +import EmailPasswordWebJs from "supertokens-web-js/recipe/emailpassword"; +import SessionWebJs from "supertokens-web-js/recipe/session"; +import { appInfo } from "./appInfo"; +import type { SuperTokensConfig } from "supertokens-web-js/types"; + +export const frontendConfig = (): SuperTokensConfig => { + return { + appInfo, + recipeList: [EmailPasswordWebJs.init(), SessionWebJs.init()], + }; +}; +``` + + + + +## 5. Create a backend config function + +```tsx title="app/config/backend.ts" check=false reason="Requires surrounding framework application context" +import SuperTokens from "supertokens-node"; +import EmailPasswordNode from "supertokens-node/recipe/emailpassword"; +import SessionNode from "supertokens-node/recipe/session"; +import { appInfo } from "./appInfo"; +import type { TypeInput } from "supertokens-node/types"; + +export const backendConfig = (): TypeInput => { + return { + framework: "custom", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo, + recipeList: [EmailPasswordNode.init(), SessionNode.init()], + isInServerlessEnv: true, + }; +}; + +let initialized = false; +export function ensureSuperTokensInit() { + if (!initialized) { + SuperTokens.init(backendConfig()); + initialized = true; + } +} +``` + +`ensureSuperTokensInit` initializes SuperTokens once before an API route uses the backend SDK. + + + + +## 6. Call the frontend `init` functions and wrap with `` component + +- Create a client component `/app/components/supertokensProvider.tsx`. This file will initialise SuperTokens and wrap its children with the `SuperTokensWrapper` component +- Modify the `/app/layout.tsx` file to use the `SuperTokensProvider` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). +- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/layout.tsx) + +```tsx title="/app/components/supertokensProvider.tsx" check=false reason="Requires surrounding framework application context" +"use client"; +import type { ReactNode } from "react"; +import { SuperTokensWrapper } from "supertokens-auth-react"; +import SuperTokensReact from "supertokens-auth-react"; +import { frontendConfig, setRouter } from "../config/frontend"; +import { usePathname, useRouter } from "next/navigation"; + +if (typeof window !== "undefined") { + // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' + SuperTokensReact.init(frontendConfig()); +} + +interface SuperTokensProviderProps { + children: ReactNode; +} + +export function SuperTokensProvider({ children }: SuperTokensProviderProps) { + setRouter(useRouter(), usePathname() || window.location.pathname); + + return {children}; +} +``` + +```tsx title="/app/layout.tsx" check=false reason="Requires surrounding framework application context" +import "./globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { SuperTokensProvider } from "./components/supertokensProvider"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +``` + + + + + +## 6. Call the frontend `init` functions and wrap with `` component + +- Create a client component `/app/components/supertokensInit.tsx`. This file will initialise SuperTokens. +- Modify the `/app/layout.tsx` file to use the `SuperTokensInit` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). + +```tsx title="/app/components/supertokensInit.tsx" check=false reason="Requires surrounding framework application context" +"use client"; +import type { ReactNode } from "react"; +import SuperTokensWebJs from "supertokens-web-js"; +import { frontendConfig } from "../config/frontend"; + +if (typeof window !== "undefined") { + // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' + SuperTokensWebJs.init(frontendConfig()); +} + +interface SuperTokensInitProps { + children: ReactNode; +} + +export function SuperTokensInit({ children }: SuperTokensInitProps) { + return <>{children}; +} +``` + +```tsx title="/app/layout.tsx" check=false reason="Requires surrounding framework application context" +import "./globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { SuperTokensInit } from "./components/supertokensInit"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} +``` + + diff --git a/docs/integrations/nextjs/app-directory/meta.ts b/docs/integrations/nextjs/app-directory/meta.ts new file mode 100644 index 0000000000..b3e005d457 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/meta.ts @@ -0,0 +1,6 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "App Directory", + order: 10, +}); diff --git a/docs/integrations/nextjs/app-directory/next-steps.mdx b/docs/integrations/nextjs/app-directory/next-steps.mdx new file mode 100644 index 0000000000..c1624956c8 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/next-steps.mdx @@ -0,0 +1,23 @@ +--- +title: 7. Next steps +description: Set up SuperTokens core for your app using managed service or self-hosting options. +sidebar: + order: 8 +--- + +## Setting up the core and database + + + +You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: +- [Managed service](/quickstart#3-configure-the-core-service) +- [Self hosted](/deployment/self-host-supertokens) + + + + +:::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] +::: + + + diff --git a/docs/integrations/nextjs/app-directory/protecting-backend/meta.ts b/docs/integrations/nextjs/app-directory/protecting-backend/meta.ts new file mode 100644 index 0000000000..0c89d09e57 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/protecting-backend/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "5. Checking Sessions in API Routes", + icon: "shield-check", + order: 6, +}); diff --git a/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx b/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx new file mode 100644 index 0000000000..d93b3e2d40 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx @@ -0,0 +1,90 @@ +--- +title: Using Next.js Proxy +description: Implement session verification in Next.js Proxy to manage user authentication and retrieve user IDs. +sidebar: + order: 2 +--- + + +This method is an alternative method for using sessions in an API. If you are already using [session guards](./session-verification-session-guard), you can skip this step. +If you are checking OAuth2 access tokens use your OAuth2/OIDC library instead of the SuperTokens Session SDK. + +## Setting up Proxy + +In Next.js 16, request interception uses `proxy.ts`. Earlier Next.js versions called this file `middleware.ts`. The Proxy checks for a session with `withSession` and forwards the user's ID to Route Handlers through an internal request header. You can forward other information in the same way. + +:::warning +You cannot pass the full session container through Proxy because request headers can only contain strings. If you need the full session container in your APIs, use [session guards](./session-verification-session-guard). +::: + +```tsx title="proxy.ts" check=false reason="Requires surrounding framework application context" +import { withSession } from "supertokens-node/nextjs"; +import { NextResponse } from "next/server"; +import type { NextRequest } from "next/server"; +import { ensureSuperTokensInit } from "./app/config/backend"; + +ensureSuperTokensInit(); + +export async function proxy(request: NextRequest) { + const requestHeaders = new Headers(request.headers); + + if (requestHeaders.has("x-user-id")) { + console.warn("The FE tried to pass x-user-id, which is only supposed to be a backend internal header. Ignoring."); + requestHeaders.delete("x-user-id"); + } + + if (request.nextUrl.pathname.startsWith("/api/auth")) { + // SuperTokens exposes /api/auth/*, so do not run session verification for these routes. + return NextResponse.next({ request: { headers: requestHeaders } }); + } + + return withSession( + request, + async (err, session) => { + if (err) { + console.error("Session verification failed", { + method: request.method, + pathname: request.nextUrl.pathname, + }); + return new NextResponse("Internal server error", { status: 500 }); + } + if (session !== undefined) { + requestHeaders.set("x-user-id", session.getUserId()); + } + + return NextResponse.next({ request: { headers: requestHeaders } }); + }, + { sessionRequired: false }, + ); +} + +export const config = { + matcher: "/api/:path*", +}; +``` + +## Fetching the user ID in your APIs + +Proxy runs for the API routes matched by `/api/:path*`. Route Handlers can read the information it forwards: + +```tsx title="app/api/userid/route.ts" check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import { ensureSuperTokensInit } from "../../config/backend"; + +ensureSuperTokensInit(); + +export function GET(request: NextRequest) { + const userId = request.headers.get("x-user-id"); + + // Proxy only adds the user ID if a session exists + if (userId === null) { + return new NextResponse("Authentication required", { status: 401 }); + } + + return NextResponse.json({ + userId, + }); +} +``` + +This creates a `GET` request for the `/api/userid` route which returns the user ID of the currently signed-in user. diff --git a/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx b/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx new file mode 100644 index 0000000000..f3796b810b --- /dev/null +++ b/docs/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx @@ -0,0 +1,51 @@ +--- +title: Adding a session guard to each API route +description: Implement session guards for API routes to manage user authentication and session validation. +sidebar: + order: 1 +--- + +:::warning[OAuth2 token verification] +Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. +::: + +:::note[This is applicable for when the frontend calls an API in the `/app/api` folder.] +::: + +For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. + +Create a new file `/app/api/user/route.ts` + +- An example of this is [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/user/route.ts). + +```ts title="app/api/user/route.ts" check=false reason="Requires surrounding framework application context" +import { withSession } from "supertokens-node/nextjs"; +import { NextResponse, NextRequest } from "next/server"; +import { ensureSuperTokensInit } from "../../config/backend"; + +ensureSuperTokensInit(); + +export function GET(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + if (!session) { + return new NextResponse("Authentication required", { status: 401 }); + } + + return NextResponse.json({ + note: "Fetch any data from your application for authenticated user after using verifySession middleware", + userId: session.getUserId(), + sessionHandle: session.getHandle(), + accessTokenPayload: session.getAccessTokenPayload(), + }); + }); +} +``` + +In the above snippet we are creating a `GET` handler for the `/api/user` route. We call the `withSession` helper function. The function will pass the session object in the callback which we then use to read user information. If a session does not exist `undefined` will be passed instead. + +The `withSession` guard will return: +- Status `401` if the session does not exist or has expired +- Status `403` if the session claims fail their validation. For example if email verification is required but the user's email is not verified. diff --git a/docs/integrations/nextjs/app-directory/protecting-route.mdx b/docs/integrations/nextjs/app-directory/protecting-route.mdx new file mode 100644 index 0000000000..8881d04a67 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/protecting-route.mdx @@ -0,0 +1,375 @@ +--- +title: 4. Checking for sessions in frontend routes +description: Learn to protect frontend routes by checking user sessions and handling session-related events. +sidebar: + order: 5 +--- + +:::warning[OAuth2 token verification] +Check authentication with your OAuth2/OIDC library when using Unified Login. +::: + +Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page. + + + + + +## Sessions with Client Components + +Lets create a client component for the `/` route of our website. + +### Using the `SessionAuth` wrapper component + +```tsx title="app/components/homeClientComponent.tsx" +"use client"; + +import { SessionAuth } from "supertokens-auth-react/recipe/session"; + +export const HomeClientComponent = () => { + return ( + +
Hello world
+
+ ); +}; +``` + +`SessionAuth` is a component exposed by the SuperTokens React SDK, it checks if a session exists and if it does not exist it will redirect the user to the login page. It also does session claim checking on the frontend and take appropriate action if the claim validators fail. For example, if you have set the email verification recipe to be `"REQUIRED"`, and the user's email is not verified, this component will redirect the user to the email verification page. + +:::warning[At the moment the `SessionAuth` component does not support server side rendering and will only work on the client side. On the server side, this component renders an empty screen.] + +Refer to the next section of this page to learn how to use sessions on the server side. +::: + +### Using `useSessionContext` + +```tsx title="app/components/homeClientComponent.tsx" +"use client"; + +import { useSessionContext } from "supertokens-auth-react/recipe/session"; + +export const HomeClientComponent = () => { + const session = useSessionContext(); + + if (session.loading) { + return
Loading...
; + } + + if (session.doesSessionExist === false) { + return
Session does not exist
; + } + + return ( +
+
+

+ Client side component got userId: {session.userId} +
+

+
+
+ ); +}; +``` + +`useSessionContext` lets you access the session information on the client side using the React Context API. `session.loading` indicates if the session is currently being loaded into the context, this will also refresh the session for you if it is expired. You can use `session.doesSessionExist` to check if a valid session exists and handle it accordingly. + +:::info[`useSessionContext` does not need to be used along with `SessionAuth`. Since our app is wrapped by the `SuperTokensWrapper` component, the `useSessionContext` hook can be used in any of our components.] +::: + +:::tip[Test by navigating to `/`] +You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. +::: + +## Sessions with Server Components + +### Creating some helper Components + +#### Creating a wrapper around `SessionAuth` +Let's say we want to protect the home page of your website (`/` route). First we will create a wrapper around the `SessionAuth` component to add the `"use client"` directive on top. + +```tsx title="app/components/sessionAuthForNextJS.tsx" +"use client"; + +import React, { useState, useEffect } from "react"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; + +type Props = Parameters[0] & { + children?: React.ReactNode | undefined; +}; + +export const SessionAuthForNextJS = (props: Props) => { + const [loaded, setLoaded] = useState(false); + useEffect(() => { + setLoaded(true); + }, []); + if (!loaded) { + return props.children; + } + return {props.children}; +}; +``` + +This is a client component that renders just its children on the server side and renders the children wrapped with `SessionAuth` on the client side. This way, the server side returns the page content, and on the client, the same page content is wrapper with `SessionAuth` which will handle session related events on the frontend - for example, if the user's session expires whilst they are on this page, then `SessionAuth` will auto redirect them to the login page. + +#### Creating the `TryRefreshComponent` + +This component will refresh the user's session if their current session has expired. + +```tsx title="app/components/tryRefreshClientComponent.tsx" +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Session from "supertokens-auth-react/recipe/session"; +import SuperTokens from "supertokens-auth-react"; + +export const TryRefreshComponent = () => { + const router = useRouter(); + const [didError, setDidError] = useState(false); + + useEffect(() => { + /** + * `attemptRefreshingSession` will call the refresh token endpoint to try and + * refresh the session. This will throw an error if the session cannot be refreshed. + */ + void Session.attemptRefreshingSession() + .then((hasSession) => { + /** + * If the user has a valid session, we reload the page to restart the flow + * with valid session tokens + */ + if (hasSession) { + router.refresh(); + } else { + SuperTokens.redirectToAuth(); + } + }) + .catch(() => { + setDidError(true); + }); + }, [router]); + + /** + * We add this check to make sure we handle the case where the refresh API fails with + * an unexpected error + */ + if (didError) { + return
Something went wrong, please reload the page
; + } + + return
Loading...
; +}; +``` + +### Using `SessionAuthForNextJS` and checking for sessions + +We then create a server component that can check if the session exists and return any session information we may need: + +```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getSSRSession } from "supertokens-node/nextjs"; + +import { TryRefreshComponent } from "./tryRefreshClientComponent"; +import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; +import { ensureSuperTokensInit } from "../config/backend"; + +ensureSuperTokensInit(); + +export async function HomePage() { + const cookieStore = await cookies(); + const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); + + if (error) { + console.error("Unable to read the SSR session", { component: "HomePage" }); + return
Unable to verify your session. Please try again.
; + } + + // `accessTokenPayload` will be undefined if it the session does not exist or has expired + if (accessTokenPayload === undefined) { + if (!hasToken) { + /** + * This means that the user is not logged in. If you want to display some other UI in this + * case, you can do so here. + */ + return redirect("/auth"); + } + + /** + * This means that the session does not exist but we have session tokens for the user. In this case + * the `TryRefreshComponent` will try to refresh the session. + * + * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + */ + return ; + } + + /** + * SessionAuthForNextJS will handle proper redirection for the user based on the different session states. + * It will redirect to the login page if the session does not exist etc. + */ + return ( + +
Your user id is: {accessTokenPayload.sub}
+
+ ); +} +``` + +The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. + +And then we can modify the `/app/page.tsx` file to use our server component + +```tsx title="app/page.tsx" check=false reason="Requires surrounding framework application context" +import styles from "./page.module.css"; +import { HomePage } from "./components/home"; + +export default function Home() { + return ( +
+ +
+ ); +} +``` + +:::tip[Test by navigating to `/`] +You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. +::: + +:::note +An example of this can be seen [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/page.tsx). +::: + +
+ + + +## Sessions with Client Components + +Checking for sessions in client components involves: + +- Using the `Session` recipe to manually check if a session exists, rendering some default UI while you check. +- Render your UI if a session exists. + +To learn more about how to do this refer to [this page](/additional-verification/session-verification/protect-frontend-routes). + +## Sessions with Server Components + +### Creating a helper component for session refreshing + +Lets start by creating a component that will refresh the session if it exists and has expired. + +```tsx title="app/components/tryRefreshClientComponent.tsx" +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import Session from "supertokens-web-js/recipe/session"; + +export const TryRefreshComponent = () => { + const router = useRouter(); + const [didError, setDidError] = useState(false); + + useEffect(() => { + void Session.attemptRefreshingSession() + .then((hasSession) => { + if (hasSession) { + router.refresh(); + } else { + /** + * This means that the session is expired and cannot be refreshed. + * In this example we redirect the user back to the login page. + */ + router.replace("/auth"); + } + }) + .catch(() => { + setDidError(true); + }); + }, [router]); + + if (didError) { + return
Something went wrong, please reload the page
; + } + + return
Loading...
; +}; +``` + +`Session.attemptRefreshingSession` will call the refresh endpoint. `hasSession` will be: +- `true` if the session was refreshed +- `false` if the session could not be refreshed + +### Modify home page to check for sessions + +Lets modify the Home page server component we created earlier: + +```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getSSRSession } from "supertokens-node/nextjs"; + +import { TryRefreshComponent } from "./tryRefreshClientComponent"; +import { ensureSuperTokensInit } from "../config/backend"; + +ensureSuperTokensInit(); + +export async function HomePage() { + const cookieStore = await cookies(); + const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); + + if (error) { + console.error("Unable to read the SSR session", { component: "HomePage" }); + return
Unable to verify your session. Please try again.
; + } + + // `accessTokenPayload` will be undefined if it the session does not exist or has expired + if (accessTokenPayload === undefined) { + if (!hasToken) { + /** + * This means that the user is not logged in. If you want to display some other UI in this + * case, you can do so here. + */ + return redirect("/auth"); + } + + /** + * This means that the session does not exist but we have session tokens for the user. In this case + * the `TryRefreshComponent` will try to refresh the session. + * + * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + */ + return ; + } + + return
Your user id is: {accessTokenPayload.sub}
; +} +``` + +The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. + +And then we can modify the `/app/page.tsx` file to use our server component + +```tsx title="app/page.tsx" check=false reason="Requires surrounding framework application context" +import styles from "./page.module.css"; +import { HomePage } from "./components/home"; + +export default function Home() { + return ( +
+ +
+ ); +} +``` + +:::tip[Test by navigating to `/`] +You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. + +For custom UI SuperTokens provides no login UI, the code above will redirect the user to the `/auth` route but you will have to build some UI that is served on that route. +::: + +
diff --git a/docs/integrations/nextjs/app-directory/server-components-requests.mdx b/docs/integrations/nextjs/app-directory/server-components-requests.mdx new file mode 100644 index 0000000000..82d9e4c4e9 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/server-components-requests.mdx @@ -0,0 +1,185 @@ +--- +title: 6. Making requests from Server Components +description: Learn to make API requests from server components using access tokens in Next.js. +sidebar: + order: 7 +--- + +Let's modify the Home page from the [route protection step](/integrations/nextjs/app-directory/protecting-route) to call this API. + + + + + +```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getSSRSession } from "supertokens-node/nextjs"; + +import { TryRefreshComponent } from "./tryRefreshClientComponent"; +import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; +import { appInfo } from "../config/appInfo"; +import { ensureSuperTokensInit } from "../config/backend"; + +ensureSuperTokensInit(); + +async function getAccessToken() { + const cookiesStore = await cookies(); + return cookiesStore.get("sAccessToken")?.value; +} + +export async function HomePage() { + const cookieStore = await cookies(); + const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); + const accessToken = await getAccessToken(); + + if (error) { + console.error("Unable to read the SSR session", { component: "HomePage" }); + return
Unable to verify your session. Please try again.
; + } + + // `accessTokenPayload` is undefined if the session does not exist or has expired + if (accessTokenPayload === undefined) { + if (!hasToken) { + /** + * This means that the user is not logged in. If you want to display some other UI in this + * case, you can do so here. + */ + return redirect("/auth"); + } + + /** + * This means that the session does not exist but we have session tokens for the user. In this case + * the `TryRefreshComponent` will try to refresh the session. + * + * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + */ + return ; + } + + const userInfoResponse = await fetch(new URL("/api/user", appInfo.websiteDomain), { + headers: { + /** + * We read the access token from the cookies and use it as a Bearer token when + * making network requests. + */ + Authorization: "Bearer " + accessToken, + }, + }); + + let message = ""; + + if (userInfoResponse.status === 200) { + message = `Your user id is: ${accessTokenPayload.sub}`; + } else if (userInfoResponse.status === 500) { + message = "Something went wrong"; + } else if (userInfoResponse.status === 401) { + // The TryRefreshComponent will try to refresh the session + // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + return ; + } else if (userInfoResponse.status === 403) { + // SessionAuthForNextJS will redirect based on which claim is invalid + return ; + } + + // You can use `userInfoResponse` to read the user's session information + + return ( + +
{message}
+
+ ); +} +``` + +We read the access token of the user from cookies. We can then send the access token as a header to the API. When the API calls `withSession` it will try to read the access token from the headers and if a session exists it will return the session information. + +
+ + + +```tsx title="app/components/home.tsx" check=false reason="Requires surrounding framework application context" +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getSSRSession } from "supertokens-node/nextjs"; + +import { TryRefreshComponent } from "./tryRefreshClientComponent"; +import { appInfo } from "../config/appInfo"; +import { ensureSuperTokensInit } from "../config/backend"; + +ensureSuperTokensInit(); + +async function getAccessToken() { + const cookiesStore = await cookies(); + return cookiesStore.get("sAccessToken")?.value; +} + +export async function HomePage() { + const cookieStore = await cookies(); + const { accessTokenPayload, hasToken, error } = await getSSRSession(cookieStore.getAll()); + const accessToken = await getAccessToken(); + + if (error) { + console.error("Unable to read the SSR session", { component: "HomePage" }); + return
Unable to verify your session. Please try again.
; + } + + // `accessTokenPayload` is undefined if the session does not exist or has expired + if (accessTokenPayload === undefined) { + if (!hasToken) { + /** + * This means that the user is not logged in. If you want to display some other UI in this + * case, you can do so here. + */ + return redirect("/auth"); + } + + /** + * This means that the session does not exist but we have session tokens for the user. In this case + * the `TryRefreshComponent` will try to refresh the session. + * + * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + */ + return ; + } + + const userInfoResponse = await fetch(new URL("/api/user", appInfo.websiteDomain), { + headers: { + /** + * We read the access token from the cookies and use it as a Bearer token when + * making network requests. + */ + Authorization: "Bearer " + accessToken, + }, + }); + + let message = ""; + + if (userInfoResponse.status === 200) { + message = `Your user id is: ${accessTokenPayload.sub}`; + } else if (userInfoResponse.status === 500) { + message = "Something went wrong"; + } else if (userInfoResponse.status === 401) { + // The TryRefreshComponent will try to refresh the session + // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 + return ; + } else if (userInfoResponse.status === 403) { + /** + * This means that one of the session claims is invalid. You should redirect the user to + * the appropriate page depending on which claim is invalid. + */ + return
Invalid Session Claims
; + } + + // You can use `userInfoResponse` to read the user's session information + + return
{message}
; +} +``` + +APIs that require sessions will return status: + +- `401` if there is no valid session or if the session has expired. In this case, we return the `TryRefreshComponent` component, which tries to refresh the session or redirects to the login page if the session can't be refreshed. +- `403` if one or more session claims fail validation. In this case, check which session claim failed and redirect the user accordingly. For example, refer to [protecting routes with email verification](/additional-verification/email-verification/protecting-routes) to check the email verification claim. + +
diff --git a/docs/integrations/nextjs/app-directory/setting-up-backend.mdx b/docs/integrations/nextjs/app-directory/setting-up-backend.mdx new file mode 100644 index 0000000000..ded62ff0b5 --- /dev/null +++ b/docs/integrations/nextjs/app-directory/setting-up-backend.mdx @@ -0,0 +1,53 @@ +--- +title: 3. Adding auth APIs +description: Add authentication APIs to your backend using SuperTokens for sign in and sign up functionality. +sidebar: + order: 4 +--- + +We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. + +## 1. Create the `app/api/auth/[[...path]]/route.ts` route +- Be sure to create the `auth/[[...path]]` folder in the `app/api/` folder. +- `route.ts` uses the `getAppDirRequestHandler` helper from `supertokens-node` to handle authentication APIs such as sign-up and sign-in. The full folder path should be `/app/api/auth/[[...path]]/route.ts`. +- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/auth/%5B...path%5D/route.ts). + +## 2. Expose the SuperTokens APIs + + +```tsx title="app/api/auth/[[...path]]/route.ts" check=false reason="Requires surrounding framework application context" +import { getAppDirRequestHandler } from "supertokens-node/nextjs"; +import type { NextRequest } from "next/server"; +import { ensureSuperTokensInit } from "../../../config/backend"; + +ensureSuperTokensInit(); + +const handleCall = getAppDirRequestHandler(); + +export async function GET(request: NextRequest) { + return handleCall(request); +} + +export async function POST(request: NextRequest) { + return handleCall(request); +} + +export async function DELETE(request: NextRequest) { + return handleCall(request); +} + +export async function PUT(request: NextRequest) { + return handleCall(request); +} + +export async function PATCH(request: NextRequest) { + return handleCall(request); +} + +export async function HEAD(request: NextRequest) { + return handleCall(request); +} +``` + +## 3. Use the login widget +If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/docs/integrations/nextjs/app-directory/setting-up-frontend.mdx b/docs/integrations/nextjs/app-directory/setting-up-frontend.mdx new file mode 100644 index 0000000000..845b98ebaa --- /dev/null +++ b/docs/integrations/nextjs/app-directory/setting-up-frontend.mdx @@ -0,0 +1,58 @@ +--- +title: 2. Showing the Login UI +description: Implement a login UI using SuperTokens in a Next.js application. +sidebar: + order: 3 +--- + + + + + +## 1. Create the `app/auth/[[...path]]/page.tsx` page +- Be sure to create the `auth/[[...path]]` folder in the `app` folder. +- `page.tsx` will contain the component for showing SuperTokens UI +- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/auth/%5B%5B...path%5D%5D/page.tsx). + +## 2. Create the `Auth` component: + +```tsx title="app/auth/[[...path]]/page.tsx" +"use client"; + +import { useEffect, useState } from "react"; +import { redirectToAuth } from "supertokens-auth-react"; +import SuperTokens from "supertokens-auth-react/ui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; + +export default function Auth() { + // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. + const [loaded, setLoaded] = useState(false); + useEffect(() => { + if (SuperTokens.canHandleRoute([EmailPasswordPreBuiltUI]) === false) { + redirectToAuth({ redirectBack: false }); + } else { + setLoaded(true); + } + }, []); + + if (loaded) { + return SuperTokens.getRoutingComponent([EmailPasswordPreBuiltUI]); + } + + return null; +} +``` + +## 3. Visit `/auth` page on your website + +If you see the login UI, you have completed this step. See [Customize the sign-in form](/authentication/email-password/customize-the-sign-in-form) to change its fields and appearance. + +If you cannot see the UI in your app, ask for help on [Discord](https://supertokens.com/discord). + + + + + +You need to build your own UI. See each [authentication method tutorial](/authentication/overview) for detailed instructions. + + diff --git a/docs/integrations/nextjs/meta.ts b/docs/integrations/nextjs/meta.ts new file mode 100644 index 0000000000..7f029a622e --- /dev/null +++ b/docs/integrations/nextjs/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "NextJS", + icon: "/docs-assets/img/logos/nextjs.svg", + order: 70, +}); diff --git a/docs/integrations/nextjs/pages-directory/about.mdx b/docs/integrations/nextjs/pages-directory/about.mdx new file mode 100644 index 0000000000..527b8fd480 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/about.mdx @@ -0,0 +1,54 @@ +--- +title: About +description: Integrate SuperTokens with Next.js for authentication, session verification, and route protection. +sidebar: + order: 1 +--- + + + + + + +Integrating SuperTokens with a Next.js app involves: +- Calling the frontend and backend init functions +- Adding a website page to display the auth related widgets (on `/auth` by default) +- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) +- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page +- Performing session verification: + - In your APIs + - In `getServerSideProps` + +## Try an example app +Download and run an example Next.js app quickly using the following command: + +```bash +npx create-supertokens-app@latest --frontend=next --recipe=emailpassword +``` + + + + + + +Integrating SuperTokens with a Next.js app involves: +- Calling the frontend and backend init functions +- Building the various auth flows as per the [custom UI setup guide](/quickstart#1-integrate-the-frontend-sdk). +- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) +- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page +- Performing session verification: + - In your APIs + - In `getServerSideProps` + +## Try an example app + +Download and run an example Next.js app quickly using the following command: + +```bash +npx create-supertokens-app@latest --frontend=next --recipe=emailpassword +``` + +:::note[This example app uses our pre-built UI] +::: + + diff --git a/docs/integrations/nextjs/pages-directory/init.mdx b/docs/integrations/nextjs/pages-directory/init.mdx new file mode 100644 index 0000000000..44101f228f --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/init.mdx @@ -0,0 +1,237 @@ +--- +title: 1. Configuration +description: Configure SuperTokens for authentication in your Next.js app with frontend and backend setup. +sidebar: + order: 2 +--- + + + + + +## 1. Install `supertokens` package + +```bash title="npm" +npm install supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors +``` + +```bash title="Yarn" +yarn add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors +``` + +```bash title="pnpm" +pnpm add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors +``` + +```bash title="Bun" +bun add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors +``` + + +## 2. Create configuration files +- Create a `config` folder in the root directory of your project +- Create an `appInfo.ts` inside the `config` folder. +- Create a `backendConfig.ts` inside the `config` folder. +- Create a `frontendConfig.ts` inside the `config` folder. + + +## 3. Create the `appInfo` configuration. + + +```tsx title="/config/appInfo.ts" +export const appInfo = { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", +}; +``` + + + + + +## 1. Install `supertokens` package + +```bash title="npm" +npm install supertokens-node supertokens-web-js nextjs-cors +``` + +```bash title="Yarn" +yarn add supertokens-node supertokens-web-js nextjs-cors +``` + +```bash title="pnpm" +pnpm add supertokens-node supertokens-web-js nextjs-cors +``` + +```bash title="Bun" +bun add supertokens-node supertokens-web-js nextjs-cors +``` + + +## 2. Create configuration files +- Create a `config` folder in the root directory of your project +- Create an `appInfo.ts` inside the `config` folder. +- Create a `backendConfig.ts` inside the `config` folder. +- Create a `frontendConfig.ts` inside the `config` folder. + + +## 3. Create the `appInfo` configuration. + + +```tsx title="/config/appInfo.ts" +export const appInfo = { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", +}; +``` + + + + + + +## 4. Create a frontend config function + +```tsx title="/config/frontendConfig.ts" check=false reason="Requires surrounding framework application context" +import EmailPasswordReact from "supertokens-auth-react/recipe/emailpassword"; +import SessionReact from "supertokens-auth-react/recipe/session"; +import { appInfo } from "./appInfo"; +import Router from "next/router"; + +export const frontendConfig = () => { + return { + appInfo, + recipeList: [EmailPasswordReact.init(), SessionReact.init()], + windowHandler: (oI: any) => { + return { + ...oI, + location: { + ...oI.location, + setHref: (href: string) => { + Router.push(href); + }, + }, + }; + }, + }; +}; +``` + + + + + +## 4. Create a frontend config function + +```tsx title="/config/frontendConfig.ts" check=false reason="Requires surrounding framework application context" +import EmailPasswordWebJs from "supertokens-web-js/recipe/emailpassword"; +import SessionWebJs from "supertokens-web-js/recipe/session"; +import { appInfo } from "./appInfo"; + +export const frontendConfig = () => { + return { + appInfo, + recipeList: [EmailPasswordWebJs.init(), SessionWebJs.init()], + }; +}; +``` + + + +## 5. Create a backend config function + +```tsx title="/config/backendConfig.ts" check=false reason="Requires surrounding framework application context" +import SuperTokens from "supertokens-node"; +import EmailPasswordNode from "supertokens-node/recipe/emailpassword"; +import SessionNode from "supertokens-node/recipe/session"; +import { appInfo } from "./appInfo"; +import type { TypeInput } from "supertokens-node/types"; + +export const backendConfig = (): TypeInput => { + return { + framework: "express", + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo, + recipeList: [EmailPasswordNode.init(), SessionNode.init()], + isInServerlessEnv: true, + }; +}; + +let initialized = false; + +export function ensureSuperTokensInit() { + if (!initialized) { + SuperTokens.init(backendConfig()); + initialized = true; + } +} +``` + + + + +## 6. Call the frontend `init` functions and wrap with `` component + +- Create a `/pages/_app.tsx` file. Learn more in the [Next.js Custom App documentation](https://nextjs.org/docs/pages/building-your-application/routing/custom-app). + + +```tsx title="/pages/_app.tsx" check=false reason="Requires surrounding framework application context" +import "../styles/globals.css"; +import type { AppProps } from "next/app"; +import SuperTokensReact, { SuperTokensWrapper } from "supertokens-auth-react"; + +import { frontendConfig } from "../config/frontendConfig"; + +if (typeof window !== "undefined") { + SuperTokensReact.init(frontendConfig()); +} + +function MyApp({ Component, pageProps }: AppProps) { + return ( + + + + ); +} + +export default MyApp; +``` + + + + + +## 6. Call the frontend `init` functions + +- Create a `/pages/_app.tsx` file. Learn more in the [Next.js Custom App documentation](https://nextjs.org/docs/pages/building-your-application/routing/custom-app). + + +```tsx title="/pages/_app.ts" check=false reason="Requires surrounding framework application context" +import "../styles/globals.css"; +import type { AppProps } from "next/app"; +import SuperTokensWebJs from "supertokens-web-js"; + +import { frontendConfig } from "../config/frontendConfig"; + +if (typeof window !== "undefined") { + SuperTokensWebJs.init(frontendConfig()); +} + +function MyApp({ Component, pageProps }: AppProps) { + return ; +} + +export default MyApp; +``` + + diff --git a/docs/integrations/nextjs/pages-directory/meta.ts b/docs/integrations/nextjs/pages-directory/meta.ts new file mode 100644 index 0000000000..01c6a95ef6 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/meta.ts @@ -0,0 +1,6 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Pages Directory", + order: 20, +}); diff --git a/docs/integrations/nextjs/pages-directory/next-steps.mdx b/docs/integrations/nextjs/pages-directory/next-steps.mdx new file mode 100644 index 0000000000..d41bfe2336 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/next-steps.mdx @@ -0,0 +1,23 @@ +--- +title: 6. Next steps +description: Set up SuperTokens core and database for your app with managed or self-hosted options. +sidebar: + order: 7 +--- + +## Setting up the core and database + + + +You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: +- [Managed service](/quickstart#3-configure-the-core-service) +- [Self hosted](/deployment/self-host-supertokens) + + + + +:::success[You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section.] +::: + + + diff --git a/docs/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx b/docs/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx new file mode 100644 index 0000000000..b3da4169c5 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx @@ -0,0 +1,70 @@ +--- +title: 5a. Session verification in an API call +description: Implement session verification in API calls to securely fetch user session data. +sidebar: + order: 1 +--- + +:::warning[OAuth2 token verification] +Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. +::: + +:::note[This is applicable for when the frontend calls an API in the `/pages/api` folder.] +::: + +For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. + +## 1. Create a new file `/pages/api/user.ts` + +## 2. Call the `supertokens.init` function +Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. + +```tsx title="pages/api/user.ts" check=false reason="Requires surrounding framework application context" +import supertokens from "supertokens-node"; +import { backendConfig } from "../../../config/backendConfig"; + +supertokens.init(backendConfig()); +``` + +## 3. Call the `verifySession` session function + + +```tsx title="pages/api/user.ts" check=false reason="Requires surrounding framework application context" +import { superTokensNextWrapper } from "supertokens-node/nextjs"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import supertokens from "supertokens-node"; +import { backendConfig } from "../../../config/backendConfig"; +import NextCors from "nextjs-cors"; + +supertokens.init(backendConfig()); + +export default async function user(req: any, res: any) { + // NOTE: We need CORS only if we are querying the APIs from a different origin + await NextCors(req, res, { + methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], + origin: "", + credentials: true, + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + }); + + // we first verify the session + await superTokensNextWrapper( + async (next) => { + return await verifySession()(req, res, next); + }, + req, + res, + ); + // if it comes here, it means that the session verification was successful + + return res.json({ + note: "Fetch any data from your application for authenticated user after using verifySession middleware", + userId: req.session.getUserId(), + sessionHandle: req.session.getHandle(), + userDataInAccessToken: req.session.getAccessTokenPayload(), + }); +} +``` + +- If no session exists, the API will return a `401` error to the client. In this case, the code `return res.json` will not be executed at all. +- In case the session does exist, `req.session` can be used to get session information. Learn more about this object [here](/additional-verification/session-verification/protect-api-routes#using-verify-session). diff --git a/docs/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx b/docs/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx new file mode 100644 index 0000000000..2a8df898fd --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx @@ -0,0 +1,200 @@ +--- +title: 5b. Session verification in getServerSideProps +description: Verify user sessions in Next.js using `getServerSideProps` for secure route access. +sidebar: + order: 2 +--- + +:::warning[OAuth2 token verification] +Verify OAuth2 access tokens with your OAuth2/OIDC library instead of the SuperTokens Session SDK. +::: + +:::note[This is applicable for when verifying a session in `getServerSideProps` or `getInitialProps`.] +::: + +For this guide, we will assume that we want to pass the logged in user's ID as a prop to a protected route. + +## 1. Check the session in `getServerSideProps` + +```tsx +import type { GetServerSidePropsContext } from "next"; +import { getSSRSession } from "supertokens-node/nextjs"; + +export function createGetServerSideProps(ensureSuperTokensInit: () => void) { + return async function getServerSideProps(context: GetServerSidePropsContext) { + ensureSuperTokensInit(); + const cookies = Object.entries(context.req.cookies).flatMap(([name, value]) => + value === undefined ? [] : [{ name, value }], + ); + const { accessTokenPayload, error } = await getSSRSession(cookies); + + if (error) { + throw error; + } + + if (accessTokenPayload === undefined) { + // This occurs if the token has expired or doesn't exist. + // Either way, sending this response prompts the frontend to attempt a session refresh. + // + // Case 1: Token doesn't exist + // - The refresh will fail, and the user will be redirected to the login page. + // + // Case 2: Token has expired + // - The client will call the refresh API and update the session tokens. + + return { props: { fromSupertokens: "needs-refresh" } }; + // or return {fromSupertokens: 'needs-refresh'} in case of getInitialProps + } + + return { + props: { userId: accessTokenPayload.sub }, + }; + + // or return { userId: accessTokenPayload.sub } in case of getInitialProps + }; +} +``` + +In your page module, import `ensureSuperTokensInit` from your application's backend configuration and export `getServerSideProps = createGetServerSideProps(ensureSuperTokensInit)`. This keeps session verification connected to the same SDK configuration as your authentication routes. + +:::warning[Use `getSSRSession` rather than `getSession` or `verifySession` here. The latter functions might update the session tokens, but server-side requests cannot propagate those updates through frontend request interceptors.] +::: + +## 2. Doing manual refresh on the frontend + + + + + +- The following will refresh a session if needed, for all your website pages +- This goes in the `/pages/_app.tsx` file + +```tsx title="/pages/_app.tsx" +import { useEffect, useState } from "react"; +import Session from "supertokens-auth-react/recipe/session"; +import { redirectToAuth } from "supertokens-auth-react"; +import type { AppProps } from "next/app"; + +function MyApp({ Component, pageProps }: AppProps<{ fromSupertokens: string }>) { + const [didError, setDidError] = useState(false); + + useEffect(() => { + async function doRefresh() { + try { + if (await Session.attemptRefreshingSession()) { + // post session refreshing, we reload the page. This will + // send the new access token to the server, and then + // getServerSideProps will succeed + location.reload(); + } else { + // the user's session has expired. So we redirect + // them to the login page + await redirectToAuth(); + } + } catch { + setDidError(true); + } + } + + if (pageProps.fromSupertokens === "needs-refresh") { + void doRefresh(); + } + }, [pageProps.fromSupertokens]); + + if (didError) { + return

Unable to refresh your session. Please reload the page.

; + } + + if (pageProps.fromSupertokens === "needs-refresh") { + // in case the frontend needs to refresh, we show nothing. + // Alternatively, you can show a spinner. + + return null; + } + + // the below is already there by default + return ; +} + +export default MyApp; +``` + +
+ + + +```tsx title="/pages/_app.tsx" +import { useEffect, useState } from "react"; +import Session from "supertokens-web-js/recipe/session"; +import type { AppProps } from "next/app"; + +function MyApp({ Component, pageProps }: AppProps<{ fromSupertokens: string }>) { + const [didError, setDidError] = useState(false); + + useEffect(() => { + async function doRefresh() { + try { + if (await Session.attemptRefreshingSession()) { + // post session refreshing, we reload the page. This will + // send the new access token to the server, and then + // getServerSideProps will succeed + location.reload(); + } else { + // the user's session has expired. So we redirect + // them to the login page + + // redirect to login page + window.location.assign("/login"); + } + } catch { + setDidError(true); + } + } + + if (pageProps.fromSupertokens === "needs-refresh") { + void doRefresh(); + } + }, [pageProps.fromSupertokens]); + + if (didError) { + return

Unable to refresh your session. Please reload the page.

; + } + + if (pageProps.fromSupertokens === "needs-refresh") { + // in case the frontend needs to refresh, we show nothing. + // Alternatively, you can show a spinner. + + return null; + } + + // the below is already there by default + return ; +} + +export default MyApp; +``` + +
+ +## 3. Consume the `userId` returned by getServerSideProps in your component + +On success, `getServerSideProps` returns +```tsx check=false reason="Requires surrounding application context" +{ + props: { + userId: accessTokenPayload.sub, + }, +} +``` + +Therefore, the associated page can access the `userId` like: + +```tsx +interface HomeProps { + userId: string; +} + +export default function Home({ userId }: HomeProps) { + return

Your user ID is: {userId}

; +} +``` diff --git a/docs/integrations/nextjs/pages-directory/protecting-backend/meta.ts b/docs/integrations/nextjs/pages-directory/protecting-backend/meta.ts new file mode 100644 index 0000000000..26a12c8ff3 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/protecting-backend/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Checking Sessions in API Routes", + icon: "shield-check", + order: 6, +}); diff --git a/docs/integrations/nextjs/pages-directory/protecting-route.mdx b/docs/integrations/nextjs/pages-directory/protecting-route.mdx new file mode 100644 index 0000000000..bd1cdc6ca5 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/protecting-route.mdx @@ -0,0 +1,112 @@ +--- +title: 4. Protecting a website route +description: Protect website routes by requiring user authentication and redirecting unauthenticated users to login. +sidebar: + order: 5 +--- + +:::warning +This information only applies when using **SuperTokens Session Access Tokens**. + +When implementing [Unified Login](/authentication/unified-login/introduction), check the authentication state using your OAuth2/OIDC library. +::: + + + + + +Protecting a website route means that it cannot be accessed unless a user is signed in. A signed-out user is redirected to the login page. + +Let's say we want to protect the home page of your website (`/` route). In this case, we can edit the `/pages/index.tsx` file to add an auth wrapper around your `Home` component like so: + +```tsx title="pages/index.tsx" check=false reason="Requires surrounding framework application context" +import React from "react"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import ProtectedPage from "./protectedPage"; + +export default function Home() { + return ( + // we protect ProtectedPage by wrapping it with SessionAuth + + + + ); +} +``` + +:::tip[Test by navigating to `/`] +You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. +::: + + + + + +Protecting a website route means that it cannot be accessed unless a user is signed in. A signed-out user is redirected to the login page. + +You can do this with the `doesSessionExist` function. This example assumes that your custom login page is at `/login`; change the path if your login page uses a different route. + +```tsx title="pages/index.tsx" check=false reason="Requires surrounding framework application context" +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import Session from "supertokens-web-js/recipe/session"; + +import ProtectedPage from "./protectedPage"; + +type SessionStatus = "loading" | "authenticated" | "redirecting" | "error"; + +export default function Home() { + const router = useRouter(); + const [sessionStatus, setSessionStatus] = useState("loading"); + + useEffect(() => { + let active = true; + + async function checkSession() { + try { + const sessionExists = await Session.doesSessionExist(); + if (!active) { + return; + } + + if (!sessionExists) { + setSessionStatus("redirecting"); + const didNavigate = await router.replace("/login"); + if (active && !didNavigate) { + setSessionStatus("error"); + } + return; + } + + setSessionStatus("authenticated"); + } catch { + if (active) { + setSessionStatus("error"); + } + } + } + + void checkSession(); + + return () => { + active = false; + }; + }, [router]); + + if (sessionStatus === "error") { + return
Unable to verify your session. Please try again.
; + } + + if (sessionStatus === "redirecting") { + return
Redirecting...
; + } + + if (sessionStatus === "loading") { + return
Loading...
; + } + + return ; +} +``` + +
diff --git a/docs/integrations/nextjs/pages-directory/setting-up-backend.mdx b/docs/integrations/nextjs/pages-directory/setting-up-backend.mdx new file mode 100644 index 0000000000..217fcae393 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/setting-up-backend.mdx @@ -0,0 +1,54 @@ +--- +title: 3. Adding auth APIs +description: Add authentication APIs to your backend using SuperTokens for user sign-in and sign-up. +sidebar: + order: 4 +--- + +We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. + +## 1. Create the `pages/api/auth/[[...path]].tsx` page +- Be sure to create the `auth` folder in the `pages/api/` folder. +- `[[...path]].tsx` will use the middleware exposed by `supertokens-node` which exposes all the APIs like sign in, sign up etc.. + +## 2. Expose the SuperTokens APIs + + +```tsx title="pages/api/auth/[[...path]].ts" check=false reason="Requires surrounding framework application context" +import { superTokensNextWrapper } from "supertokens-node/nextjs"; +import { middleware } from "supertokens-node/framework/express"; +import { NextApiRequest, NextApiResponse } from "next"; +import { Request, Response } from "express"; +import supertokens from "supertokens-node"; +import { backendConfig } from "../../../config/backendConfig"; +import NextCors from "nextjs-cors"; + +supertokens.init(backendConfig()); + +export default async function superTokens(req: NextApiRequest & Request, res: NextApiResponse & Response) { + await NextCors(req, res, { + methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], + origin: "", + credentials: true, + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + }); + + await superTokensNextWrapper( + async (next) => { + res.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + await middleware()(req, res, next); + }, + req, + res, + ); + if (!res.writableEnded) { + res.status(404).send("Not found"); + } +} +``` + +:::note[In the snippet above we add the `Cache-Control` header to the responses for all auth APIs. This is required if you are deploying your app with Vercel because API responses are automatically cached for production deployments. This results in problems because APIs such as `/session/refresh` return older session tokens resulting in infinite calls to refresh if an API returns unauthorised status. Setting the header ensures that Vercel does not cache any of the auth API responses.] +::: + +## 3. Use the login widget +If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/docs/integrations/nextjs/pages-directory/setting-up-frontend.mdx b/docs/integrations/nextjs/pages-directory/setting-up-frontend.mdx new file mode 100644 index 0000000000..a71bb41078 --- /dev/null +++ b/docs/integrations/nextjs/pages-directory/setting-up-frontend.mdx @@ -0,0 +1,54 @@ +--- +title: 2. Showing Login UI +description: Implement a login UI using SuperTokens in a Next.js application. +sidebar: + order: 3 +--- + + + + + +## 1. Create the `pages/auth/[[...path]].tsx` page +- Be sure to create the `auth` folder in the `pages` folder. +- `[[...path]].tsx` will contain the component for showing SuperTokens UI + +## 2. Create the `Auth` component: + +```tsx title="pages/auth/[[...path]].tsx" +import React, { useEffect } from "react"; +import dynamic from "next/dynamic"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { redirectToAuth } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; + +const SuperTokensComponentNoSSR = dynamic<{}>( + new Promise((res) => res(() => getRoutingComponent([EmailPasswordPreBuiltUI]))), + { ssr: false }, +); + +export default function Auth() { + // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. + useEffect(() => { + if (canHandleRoute([EmailPasswordPreBuiltUI]) === false) { + redirectToAuth(); + } + }, []); + + return ; +} +``` + +## 3. Visit `/auth` page on your website + +If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre-built UI, for each page on [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground). + +If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) + + + + + +You need to build your own UI. You will have to check each [authentication method tutorial](/authentication/overview) for detailed instructions how how to achieve this. + + diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx new file mode 100644 index 0000000000..84460ef1d4 --- /dev/null +++ b/docs/integrations/overview.mdx @@ -0,0 +1,59 @@ +--- +title: Overview +description: Explore different integration guides that present how to use SuperTokens with different platforms and frameworks. +sidebar: + order: 10 +--- + +Explore different integration guides that present how to use SuperTokens with different platforms and frameworks. + +--- + + +## Frameworks + + + + + + + + + + + + + + + + + + + + + + + + +## Cloud Platforms + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/integrations/supabase.mdx b/docs/integrations/supabase.mdx new file mode 100644 index 0000000000..53eaee7d67 --- /dev/null +++ b/docs/integrations/supabase.mdx @@ -0,0 +1,438 @@ +--- +title: Supabase +description: Integrate SuperTokens with Supabase in a Next.js app for secure user data management. +sidebar: + icon: /docs-assets/img/logos/supabase.svg + order: 80 +--- + +## Overview + +The following guide shows you how to integrate a Next.js app with SuperTokens and Supabase. +It includes instructions on how to: +- Create a Supabase project with a table to store your user data +- Create a Supabase JWT and store the user's session +- Enable row level security policies in your Supabase table to ensure only authorized users can access their data + +In this example, the user's email is stored mapped to their SuperTokens userId in Supabase. + +You can also check an [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-supabase) for specific references. + +## Before you start + +The guide does not include instructions on how to setup a Next.js app with SuperTokens. +To do this you can follow the [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) instructions. + +## Steps + +### 1. Configure Supabase + +Supabase provides a database with authentication and authorization features. This guide uses Supabase to store the user's info mapped to their SuperTokens `userId`. + +#### 1.1 Create a new Supabase project + +1. From your [Supabase dashboard](https://app.supabase.com/), click New project. +2. Enter a Name for your Supabase project. +3. Enter a secure Database Password. +4. Select the same Region you host your app's backend in. +5. Click Create new project. + +![Supabase dashboard](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_dashboard_create.png) + +#### 1.2 Create the user table in Supabase + +1. From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click Table editor, then New table. +2. Enter `users` as the `Name` field. +3. Select `Enable Row Level Security (RLS).` +4. Remove the default columns +5. Create two new columns: + - `user_id` as `varchar` as primary key + - email as `varchar` +6. Click `Save` to create the new table. + +![Supabase table create](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_table_create.png) + +### 2. Setup JWT creation + +In this section, the SuperTokens backend is overridden to create a JWT signed with Supabase's secret which contains the user's `userId`. +This token is used on the frontend and backend to read and write to Supabase's database. + +#### 2.1 Integrate your Next.js app with SuperTokens + +Follow either the [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) guides for instructions on how to configure your application. + +#### 2.2 Save the Supabase configuration values + +Retrieve the Supabase configuration values from the dashboard and add them to your `.env` file: + +```bash +// retrieve the following from your supabase dashboard + +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_KEY= +SUPABASE_SIGNING_SECRET= +``` + +#### 2.3 Create the Supabase JWT + +In the Next.js app when a user signs up, you'll want to store the user's email in Supabase. +The email can then be retrieved from Supabase and displayed on the frontend. +To use the Supabase client to query the database, you need to create a JWT signed with your Supabase app's signing secret. +This JWT also needs to contain the user's `userId` so Supabase knows an authorized user is making the request. +To create this flow, SuperTokens needs to be modified so that, when a user signs up or signs in, a JWT signed with Supabase's signing secret is created and attached to the user's session. +Attaching the JWT to the user's session allows the Supabase JWT to be retrieved on the frontend and backend (post session verification), which can then be used to query Supabase. + +To create the JWT signed with Supabase's signing secret, the `jsonwebtoken` library is used. + +```bash +npm install jsonwebtoken +``` + +The JWT can be added to the user's session by overriding the `createNewSession` function and adding it to the `accessTokenPayload` + +```ts +// config/backendConfig.ts + +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import SessionNode from "supertokens-node/recipe/session"; +import { TypeInput, AppInfo } from "supertokens-node/types"; +import jwt from "jsonwebtoken"; + +let appInfo: AppInfo = { + appName: "TODO: add your app name", + apiDomain: "TODO: add your website domain", + websiteDomain: "TODO: add your website domain", +}; + +let supabase_signing_secret = process.env.SUPABASE_SIGNING_SECRET || "TODO: Your Supabase Signing Secret"; + +let backendConfig = (): TypeInput => { + return { + framework: "express", + supertokens: { + connectionURI: "https://try.supertokens.com", + }, + appInfo, + recipeList: [ + EmailPassword.init({ + /*...*/ + }), + SessionNode.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + createNewSession: async function (input) { + const payload = { + userId: input.userId, + exp: Math.floor(Date.now() / 1000) + 60 * 60, + }; + + const supabase_jwt_token = jwt.sign(payload, supabase_signing_secret); + + input.accessTokenPayload = { + ...input.accessTokenPayload, + supabase_token: supabase_jwt_token, + }; + + return await originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], + isInServerlessEnv: true, + }; +}; +``` + +### 3. Create a Supabase client + +A client is created to interact with Supabase using the `supabase-js` library. + +#### 3.1 Install the `supabase-js` library + +```bash +npm install @supabase/supabase-js +``` + +#### 3.2 Create a new file called `utils/supabase.ts` and add the following: + +```ts check=false reason="Requires surrounding application context" +// utils/supabase.ts + +import { createClient } from "@supabase/supabase-js"; + +let supabase_url = process.env.NEXT_PUBLIC_SUPABASE_URL || "TODO: Your Supabase URL"; +let supabase_key = process.env.NEXT_PUBLIC_SUPABASE_KEY || "TODO: Your Supabase Key"; + +const getSupabase = (access_token: string) => { + const supabase = createClient(supabase_url, supabase_key); + + supabase.auth.session = () => ({ + access_token, + token_type: "", + user: null, + }); + + return supabase; +}; + +export { getSupabase }; +``` + +### 4. Insert users into Supabase when they sign up + +In this example app, the user can sign up via Email-Password authentication. +The API needs to be overridden such that when a user signs up, their email mapped to their userId is stored in Supabase. + +#### 4.1 Override the Email-Password sign up function + +```ts check=false reason="This configuration fragment uses a recipe placeholder that must be replaced for the chosen authentication method." +// config/backendConfig.ts + +let appInfo: AppInfo = { + appName: "TODO: add your app name", + apiDomain: "TODO: add your website domain", + websiteDomain: "TODO: add your website domain" +} + +// take a look at the Creating Supabase Client section to see how to define getSupabase +let getSupabase: any; + +let backendConfig = (): TypeInput => { + return { + framework: "express", + supertokens: { + connectionURI: "https://try.supertokens.com", + }, + appInfo, + recipeList: [ + .init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + // the signUpPOST function handles sign up + signUpPOST: async function (input) { + if (originalImplementation.signUpPOST === undefined) { + throw Error("Should never come here"); + } + + let response = await originalImplementation.signUpPOST(input); + + if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { + + // retrieve the accessTokenPayload from the user's session + const accessTokenPayload = response.session.getAccessTokenPayload(); + + // create a supabase client with the supabase_token from the accessTokenPayload + const supabase = getSupabase(accessTokenPayload.supabase_token); + + // store the user's email mapped to their userId in Supabase + const { error } = await supabase + .from("users") + .insert({ email: response.user.emails[0], user_id: response.user.id }); + + if (error !== null) { + + throw error; + } + } + + return response; + }, + }; + }, + }, + }), + SessionNode.init({/*...*/}), + ], + isInServerlessEnv: true, + }; +}; + +``` + +The Email-Password sign up flow is changed by overriding the `signUpPOST` API. +When a user signs up, the `supabase_token` is retrieved from the user's `accessTokenPayload`(this was added in the previous step where the `createNewSession` function was changed) and used to query Supabase to insert the new user's information. + + +### 5. Retrieve the user email on the frontend + + + + + +With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. + +```tsx +// pages/index.tsx + +import React, { useState, useEffect } from "react"; +import Head from "next/head"; +import { SessionAuth, useSessionContext } from "supertokens-auth-react/recipe/session"; + +// take a look at the Creating Supabase Client section to see how to define getSupabase +let getSupabase: any; + +export default function Home() { + return ( + // The ProtectedPage component is wrapped with the SessionAuth so only an + // authenticated user can access it. + + + + ); +} + +function ProtectedPage() { + // retrieve the authenticated user's accessTokenPayload and userId from the sessionContext + const session = useSessionContext(); + + const [userEmail, setEmail] = useState(""); + useEffect(() => { + async function getUserEmail() { + if (session.loading) { + return; + } + // retrieve the supabase client who's JWT contains users userId, this is + // used by supabase to check that the user can only access table entries which contain their own userId + const supabase = getSupabase(session.accessTokenPayload.supabase_token); + + // retrieve the user's name from the users table whose email matches the email in the JWT + const { data } = await supabase.from("users").select("email").eq("user_id", session.userId); + + if (data.length > 0) { + setEmail(data[0].email); + } + } + getUserEmail(); + }, [session]); + + if (session.loading) { + return null; + } + + return ( +
+ + SuperTokens 💫 + + + +
+

+ You are authenticated with SuperTokens! (UserId: {session.userId}) +
+ Your email retrieved from Supabase: {userEmail} +

+
+
+ ); +} +``` + +
+ + + +With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. + +```tsx +import Session from "supertokens-web-js/recipe/session"; + +// take a look at the Creating Supabase Client section to see how to define getSupabase +let getSupabase: any; + +async function getEmailFromSupabase() { + if (await Session.doesSessionExist()) { + let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); + const supabase = getSupabase(accessTokenPayload.supabase_token); + + const { data } = await supabase + .from("users") + .select("email") + .eq("user_id", await Session.getUserId()); + + if (data.length > 0) { + return data[0].email; + } + return undefined; + } + throw new Error("Session does not exist"); +} +``` + + + +As seen above, the access token payload is fetched from SuperTokens to retrieve the authenticated user's Supabase access token which can be used to fetch the user's email from Supabase. + +### 6. Enforce row level security for select and insert requests + +To enforce Row Level Security for the Users table, you need to create policies for Select and Insert requests. +These polices retrieve the `userId` from the JWT and check if it matches the `userId` in the Supabase table. +A PostgreSQL function is needed to extract the `userId` from the JWT. + +The payload in the JWT has the following structure: + +```bash +{ + userId, + exp +} +``` + +#### 6.1 Create PostgreSQL function to retrieve `userId` from JWT + +To create the PostgreSQL function, navigate back to the Supabase dashboard, select `SQL` from the sidebar menu, and click `New query`. This creates a new query called `new sql snippet`, which allows you to run any SQL against the Postgres database. +Write the following and click `Run`. + + ```bash + create or replace function auth.user_id() returns text as $$ + select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text; + $$ language sql stable; + ``` + +- This creates a function called `auth.user_id()`, which inspects the `userId` field of our JWT payload. + +#### 6.2 Create Policies for `SELECT` and `INSERT` queries: + +##### `SELECT` query policy + +The first policy checks whether the user is the owner of the email being retrieved. + +- Select `Authentication` from the Supabase sidebar menu, click `Policies`, and then `New Policy` on the `Users` table. + + ![Create policy](/docs-assets/img/thirdpartyemailpassword/supabase/create_policy.png) + +- From the modal, select `Create a policy from scratch` and add the following. + + ![select policy](/docs-assets/img/thirdpartyemailpassword/supabase/policy_config_select.png) + +- This policy is calling the PostgreSQL function we just created to get the currently logged in user's ID `auth.user_id()` and checking whether this matches the `user_id` column for the current `email`. If it does, then it allows the user to select it, otherwise it continues to deny. + +- Click `Review` and then `Save policy`. + +##### `INSERT` query policy + +The second policy checks whether the `user_id` being inserted is the same as the `userId` in the JWT. + +- Create another policy and add the following: + + ![insert policy](/docs-assets/img/thirdpartyemailpassword/supabase/policy_config_insert.png) + +Similar to the previous policy, the PostgreSQL function that was created is called to get the currently logged in user's ID and check whether this matches the `user_id` column for the row being inserted. If it does, then it allows the user to insert the row, otherwise it continues to deny. + +Click `Review` and then `Save policy`. + +### 6.3 Test your changes + +You can now sign up and you should see the following screen: + +![auth screen](/docs-assets/img/thirdpartyemailpassword/supabase/supabase_app_authenticated_screen.png) + +If you navigate to your table you should see a new row with the user's `user_id` and `email`. + +![table with user](/docs-assets/img/thirdpartyemailpassword/supabase/table_with_user.png) diff --git a/docs/integrations/vercel.mdx b/docs/integrations/vercel.mdx new file mode 100644 index 0000000000..a6446d2da3 --- /dev/null +++ b/docs/integrations/vercel.mdx @@ -0,0 +1,61 @@ +--- +title: Vercel +description: 'Learn how to integrate SuperTokens with Vercel ' +sidebar: + icon: /docs-assets/img/logos/vercel.svg +--- + +This page only talks about what environment variables to use when you are deploying an application on Vercel. +For a full set of instructions on how to integrate **SuperTokens** in a **Next.js** project, please see either our [app router](/integrations/nextjs/app-directory/about) or [pages router](/integrations/nextjs/pages-directory/about) guides. + + +## Working with Vercel's inspect and production URL + +Vercel provides one production URL per app and one unique inspect URL per deployment. +To get SuperTokens to work with dynamic URLs, you need to make the following changes to the [`appInfo` object](/references/frontend-sdks/reference#sdk-configuration): + +### On the frontend + + + + + + +```text +appInfo = { + apiDomain: window.location.origin, + websiteDomain: window.location.origin, + ... +} +``` + + + + + + + +```text +appInfo = { + apiDomain: window.location.origin + ... +} +``` + + +### On the backend + +```text +appInfo = { + apiDomain: process.env.VERCEL_URL, + websiteDomain: process.env.VERCEL_URL, + ... +}, +``` + +Vercel adds an environment variable to the backend - `VERCEL_URL`, which points to the current URL that the app is deployed on. This allows SuperTokens to work on all inspect URLs generated by Vercel without you having to keep changing your code. + +:::note[The above setting works only if your backend and frontend are deployed on the same URL. If you are using a different backend and using Vercel only for your frontend, then:] +- Set the `apiDomain` on the frontend and backend to point to your backend. +- The `websiteDomain` on the frontend should be `window.location.origin`, but on the backend, it should be equal to your production deployment URL. This will break certain features of the app for inspect URL deployments, but it will work as expected for production deployments. +::: diff --git a/docs/legacy/core/v10/self-host-supertokens.mdx b/docs/legacy/core/v10/self-host-supertokens.mdx index e2691d9733..67ec46d950 100644 --- a/docs/legacy/core/v10/self-host-supertokens.mdx +++ b/docs/legacy/core/v10/self-host-supertokens.mdx @@ -1,41 +1,36 @@ --- title: Self-host SuperTokens -hide_title: true -sidebar_position: 1 -toc_max_heading_level: 4 -description: >- - Run SuperTokens on your infrastructure, control authentication data, and scale - as needed. -page_type: guide -recipe: emailpassword -category: authentication +description: Run SuperTokens on your infrastructure, control authentication data, and scale as needed. +sidebar: + order: 1 + hidden: true --- +## Overview -# Self-host a legacy SuperTokens Core version - - - - -## Overview - -The following tutorial shows you how to self-host the legacy SuperTokens Core versions, `<= 10`. +The following tutorial shows you how to self-host legacy SuperTokens Core 10.x. Since version `11.0.0`, SuperTokens Core no longer supports MySQL as a database. ## Before you start To deploy the Core Service you must configure two things: the actual API and the database. -- You can use either **PostreSQL** or **MySQL** as a data source for the core service. +- You can use either **PostgreSQL** or **MySQL** as a data source for the Core service. - Deploy the core service with **Docker** or directly inside your VM. -- The minimum required version is **MySQL 8.0.11**. -- The minimum required version is **PostgreSQL 13.0**. +- The archived guide recommended MySQL 8.0.11 and PostgreSQL 13.0. Released source does not establish these as exact lower bounds; verify the database version against your selected Core 10.x and database-plugin release. + +:::danger +SuperTokens Core is a trusted backend component with privileged user, session, and tenant APIs. Run Core and its database +on private networks reachable only by trusted backend services. Never expose either directly to browsers or other +clients you do not trust. Core v10 has no API key by default. Configure a generated API key, restrict ingress, and terminate TLS +at a trusted reverse proxy as defense in depth. A shared API key does not replace backend tenant authorization. +::: ## Steps -:::warning no-title +:::warning -This guide is only relevant if you are using a legacy **SuperTokens Core** version, `<= 10.0.1`. +This guide is only relevant if you are using a legacy **SuperTokens Core 10.x** release. **MySQL** is no longer available as a storage option starting with the **11.0.0** version. ::: @@ -43,103 +38,103 @@ This guide is only relevant if you are using a legacy **SuperTokens Core** versi ### 1. Install SuperTokens core -#### With docker - - - +#### With Docker + + ```bash -docker run -p 3567:3567 -d supertokens/supertokens-mysql@10.1 +docker run -p 127.0.0.1:3567:3567 -d supertokens/supertokens-mysql:10.1 ``` - -- To see all the environment variables available, please see [the README file](https://github.com/supertokens/supertokens-docker-mysql/blob/master/README.md). -- The above command starts the container with an in-memory database. This means you **do not need to connect it to MySQL to test out SuperTokens**. - - - - - + + ```bash -docker run -p 3567:3567 -d supertokens/supertokens-postgresql^{docker_version_postgresql} +docker run -p 127.0.0.1:3567:3567 -d supertokens/supertokens-postgresql:10.1 ``` + + -- To see all the environment variables available, please see [the README file](https://github.com/supertokens/supertokens-docker-postgresql/blob/master/README.md). + + +- The above command starts the container with an in-memory database. This means you **do not need to connect it to MySQL to test out SuperTokens**. + + - The above command starts the container with an in-memory database. This means you **do not need to connect it to PostgreSQL to test out SuperTokens**. + + - - - -#### Without docker +#### Without Docker ##### 1. Download SuperTokens - ## Visit the [open source download page](https://SuperTokens.com/use-oss). - ## Click on the "Binary" tab. - ## Choose your database. - ## Download the SuperTokens zip file for your OS. + +Open the [open source download page](https://SuperTokens.com/use-oss). + + + + + + + -Once downloaded, remove the zip, and you see a folder named `supertokens`. +After downloading, verify the release checksum or signature and extract the archive. You should see a folder named `supertokens`. ##### 2. Install SuperTokens - - - + + ```bash -# sudo is required so that the supertokens +# sudo is required so that the supertokens # command can be added to your PATH variable. cd supertokens sudo ./install ``` - - - - + + ```bash cd supertokens ./install ``` -:::caution -You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above. -::: - - - - - + + ```batch -Rem run as an Administrator. This is required so that the supertokens +Rem run as an Administrator. This is required so that the supertokens Rem command can be added to your PATH. cd supertokens install.bat ``` - - + + -:::important -After installing, you can delete the downloaded folder as you no longer need it. + + +:::warning[You may get an error like `java cannot be opened because the developer cannot be verified`. To solve this, visit System Preferences > Security & Privacy > General Tab, and then click on the Allow button at the bottom. Then retry the command above.] +::: + + + +:::note[After installing, you can delete the downloaded folder as you no longer need it.] Make any changes to the configuration in the `config.yaml` file in the installation directory, as specified in the output of the `supertokens --help` command. ::: -##### 3. Start the core service +##### 3. Start the core service Running the following command starts the service. ```bash supertokens start [--host=...] [--port=...] ``` -- The above command starts the container with an in-memory database. +- The above command starts the Core process using the configured database. - To see all available options please run `supertokens start --help` -:::info Tip +:::info[Tip] To stop the service, run the following command: ```bash supertokens stop @@ -147,197 +142,205 @@ supertokens stop ::: -### 2. Test that the service is running +### 2. Test that the service is running Open a browser and visit `http://localhost:3567/hello`. If you see a page that says `Hello` back, then the container started successfully! If you are having issues with starting the docker image, please feel free to reach out [over email](mailto:support@supertokens.com) or [via Discord](https://supertokens.com/discord). :::tip -The `/hello` route checks whether the database connection is correctly set up and only returns a 200 status code if there is no issue. - -If you are using Kubernetes or docker swarm, this endpoint is perfect for doing readiness and liveness probes. +`/hello` performs a storage query in Core v10 and can be used as a basic readiness signal. It deliberately requires no API +key, so a successful response does not prove that protected APIs require authentication or that Core is safely isolated. +Pair it with authenticated application checks and database monitoring; do not expose Core merely to reach this endpoint. ::: +### 3. Connect the backend SDK with SuperTokens -### 3. Connect the backend SDK with SuperTokens - -- The default `port` for SuperTokens is `3567`. You can change this by binding a different port in the `docker run` command. For example, `docker run -p 8080:3567` runs SuperTokens on port `8080` on your machine. +- The default port is `3567`. Keep it private. For local testing, bind it only to `127.0.0.1`, for example `-p 127.0.0.1:8080:3567`. - The connection info goes in the `supertokens` object in the `init` function on your backend: - - - + + ```tsx import supertokens from "supertokens-node"; +const apiKey = process.env.SUPERTOKENS_API_KEY; +if (apiKey === undefined || apiKey.length === 0) { + throw new Error("SUPERTOKENS_API_KEY is required"); +} + supertokens.init({ - // highlight-start - supertokens: { - connectionURI: "http://localhost:3567", - apiKey: "someKey" // OR can be undefined - }, - // highlight-end - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [] + supertokens: { + connectionURI: "http://localhost:3567", + apiKey, + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], }); ``` - - - - + + ```go -import "github.com/supertokens/supertokens-golang/supertokens" +import ( + "os" + + "github.com/supertokens/supertokens-golang/supertokens" +) func main() { + apiKey := os.Getenv("SUPERTOKENS_API_KEY") + if apiKey == "" { + panic("SUPERTOKENS_API_KEY is required") + } supertokens.Init(supertokens.TypeInput{ - // highlight-start Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "http://localhost:3567", - APIKey: "someKey", + APIKey: apiKey, }, - // highlight-end }) } ``` + + +```python check=false reason="Partial configuration example" +import os - - - -```python from supertokens_python import init, InputAppInfo, SupertokensConfig +api_key = os.environ["SUPERTOKENS_API_KEY"] +if not api_key: + raise RuntimeError("SUPERTOKENS_API_KEY is required") + init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - # highlight-start supertokens_config=SupertokensConfig( connection_uri='http://localhost:3567', - api_key='someKey' + api_key=api_key ), - # highlight-end - framework='...', # type: ignore + framework='...', recipe_list=[ #... ] ) ``` + + - - - -:::info Security -No API key exists by default. -Check [the API keys documentation](/docs/platform-configuration/supertokens-core/api-keys) to see how to add one. +:::info[Configure the same generated secret in Core and every backend] +Generate a key with `openssl rand -hex 32`, store it in a secret manager, and inject it as Core's `API_KEYS` and the +`SUPERTOKENS_API_KEY` used by the backend. Never bake it into source or an image. Core v10 keys must be at least 20 characters and +contain only alphanumeric characters, `=`, or `-`. ::: ### 4. Set up the database -#### 4.1 Create a database {{optional}} +#### 4.1 Create a database (optional) - - + + ```sql CREATE DATABASE SuperTokens; ``` - - - + + ```sql CREATE DATABASE supertokens; ``` - - - + + You can skip this step if you want SuperTokens to write to your own database. In this case, you need to provide your database's name as shown in the step below. -#### 4.2 Connect SuperTokens to your database - -##### With docker +#### 4.2 Connect SuperTokens to your database - - -:::caution -Host being `localhost` / `127.0.0.1` does not work in a docker image. Instead, please provide the database's local / public host name or IP address. +##### With Docker -You also need to make the database listen on all the IPs of the local machine. -Edit the `mysqld.cnf` configuration file and set the value of `bind-address` to `0.0.0.0`. + + +:::warning +Inside a container, `localhost` refers to that container. Put Core and MySQL on the same private Docker/VPC network and +restrict the MySQL bind address, grants, firewall, and security groups so that only Core can connect. Do not publish port +3306 or use a public database address. ::: + + +:::warning +Inside a container, `localhost` refers to that container. Put Core and PostgreSQL on the same private Docker/VPC network +and restrict `listen_addresses`, `pg_hba.conf`, firewall, and security groups so that only Core can connect. Do not publish +port 5432 or use a public database address. +::: + + + + ```bash +: "${SUPERTOKENS_API_KEY:?Set a generated Core API key}" docker run \ - -p 3567:3567 \ -//highlight-next-line + --network app-network \ -e MYSQL_CONNECTION_URI="mysql://username:pass@host/dbName" \ - -d supertokens/supertokens-mysql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-mysql:10.1 # OR docker run \ - -p 3567:3567 \ -//highlight-start + --network app-network \ -e MYSQL_USER="username" \ -e MYSQL_PASSWORD="password" \ -e MYSQL_HOST="host" \ -e MYSQL_PORT="3306" \ -e MYSQL_DATABASE_NAME="supertokens" \ -//highlight-end - -d supertokens/supertokens-mysql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-mysql:10.1 ``` - - - - -:::caution -Host being `localhost` / `127.0.0.1` does not work in a docker image. Instead, please provide the database's local / public host name or IP address. - -You also need to make the database listen on all the IPs of the local machine. -Edit the `postgresql.conf` configuration file and set the value of `listen_addresses` to `0.0.0.0`. -::: - + + ```bash +: "${SUPERTOKENS_API_KEY:?Set a generated Core API key}" docker run \ - -p 3567:3567 \ - // highlight-next-line + --network app-network \ -e POSTGRESQL_CONNECTION_URI="postgresql://username:pass@host/dbName" \ - -d supertokens/supertokens-postgresql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-postgresql:10.1 # OR docker run \ - -p 3567:3567 \ - // highlight-start + --network app-network \ -e POSTGRESQL_USER="username" \ -e POSTGRESQL_PASSWORD="password" \ -e POSTGRESQL_HOST="host" \ -e POSTGRESQL_PORT="5432" \ -e POSTGRESQL_DATABASE_NAME="supertokens" \ - // highlight-end - -d supertokens/supertokens-postgresql + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-postgresql:10.1 ``` + + -:::tip -You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option. + + +:::tip[You can also provide the table schema by setting the `POSTGRESQL_TABLE_SCHEMA` option.] ::: - - - -##### Without docker + + +##### Without Docker - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command @@ -356,10 +359,8 @@ mysql_port: 3306 mysql_database_name: "supertokens" ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command @@ -378,1179 +379,106 @@ postgresql_port: "5432" postgresql_database_name: "supertokens" ``` + + + + You can also provide the table schema by setting the `postgresql_table_schema` option. - - - - + + :::info -The required tables should create automatically if the database user has table creation permission. -If not, you can create them manually using the following snippet. - - - ## MySQL tables - -```sql -CREATE TABLE `apps` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `created_at_time` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`) -); - -CREATE TABLE `tenants` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `created_at_time` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `tenant_configs` ( - `connection_uri_domain` varchar(256) NOT NULL DEFAULT '', - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `core_config` text, - `email_password_enabled` tinyint(1) DEFAULT NULL, - `passwordless_enabled` tinyint(1) DEFAULT NULL, - `third_party_enabled` tinyint(1) DEFAULT NULL, - `is_first_factors_null` tinyint(1) DEFAULT NULL, - PRIMARY KEY (`connection_uri_domain`,`app_id`,`tenant_id`) -); - -CREATE TABLE `tenant_thirdparty_providers` ( - `connection_uri_domain` varchar(256) NOT NULL DEFAULT '', - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `third_party_id` varchar(28) NOT NULL, - `name` varchar(64) DEFAULT NULL, - `authorization_endpoint` text, - `authorization_endpoint_query_params` text, - `token_endpoint` text, - `token_endpoint_body_params` text, - `user_info_endpoint` text, - `user_info_endpoint_query_params` text, - `user_info_endpoint_headers` text, - `jwks_uri` text, - `oidc_discovery_endpoint` text, - `require_email` tinyint(1) DEFAULT NULL, - `user_info_map_from_id_token_payload_user_id` varchar(64) DEFAULT NULL, - `user_info_map_from_id_token_payload_email` varchar(64) DEFAULT NULL, - `user_info_map_from_id_token_payload_email_verified` varchar(64) DEFAULT NULL, - `user_info_map_from_user_info_endpoint_user_id` varchar(64) DEFAULT NULL, - `user_info_map_from_user_info_endpoint_email` varchar(64) DEFAULT NULL, - `user_info_map_from_user_info_endpoint_email_verified` varchar(64) DEFAULT NULL, - PRIMARY KEY (`connection_uri_domain`,`app_id`,`tenant_id`,`third_party_id`), - FOREIGN KEY (`connection_uri_domain`, `app_id`, `tenant_id`) REFERENCES `tenant_configs` (`connection_uri_domain`, `app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE TABLE `tenant_thirdparty_provider_clients` ( - `connection_uri_domain` varchar(256) NOT NULL DEFAULT '', - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `third_party_id` varchar(28) NOT NULL, - `client_type` varchar(64) NOT NULL DEFAULT '', - `client_id` varchar(256) NOT NULL, - `client_secret` text, - `scope` text, - `force_pkce` tinyint(1) DEFAULT NULL, - `additional_config` text, - PRIMARY KEY (`connection_uri_domain`,`app_id`,`tenant_id`,`third_party_id`,`client_type`), - FOREIGN KEY (`connection_uri_domain`, `app_id`, `tenant_id`, `third_party_id`) REFERENCES `tenant_thirdparty_providers` (`connection_uri_domain`, `app_id`, `tenant_id`, `third_party_id`) ON DELETE CASCADE -); - -CREATE TABLE `tenant_first_factors` ( - connection_uri_domain VARCHAR(256) DEFAULT '', - app_id VARCHAR(64) DEFAULT 'public', - tenant_id VARCHAR(64) DEFAULT 'public', - factor_id VARCHAR(128), - PRIMARY KEY (`connection_uri_domain`, `app_id`, `tenant_id`, `factor_id`), - FOREIGN KEY (`connection_uri_domain`, `app_id`, `tenant_id`) REFERENCES `tenant_configs` (`connection_uri_domain`, `app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE TABLE `tenant_required_secondary_factors` ( - connection_uri_domain VARCHAR(256) DEFAULT '', - app_id VARCHAR(64) DEFAULT 'public', - tenant_id VARCHAR(64) DEFAULT 'public', - factor_id VARCHAR(128), - PRIMARY KEY (`connection_uri_domain`, `app_id`, `tenant_id`, `factor_id`), - FOREIGN KEY (`connection_uri_domain`, `app_id`, `tenant_id`) REFERENCES `tenant_configs` (`connection_uri_domain`, `app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE TABLE `key_value` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `name` varchar(128) NOT NULL, - `value` text, - `created_at_time` bigint unsigned DEFAULT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`name`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE TABLE `app_id_to_user_id` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `recipe_id` varchar(128) NOT NULL, - `primary_or_recipe_user_id` char(36) NOT NULL, - `is_linked_or_is_a_primary_user` BOOLEAN NOT NULL DEFAULT FALSE, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE, - FOREIGN KEY (`app_id`, `primary_or_recipe_user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE INDEX app_id_to_user_id_primary_user_id_index ON `app_id_to_user_id` (`primary_or_recipe_user_id`); - -CREATE INDEX app_id_to_user_id_user_id_index ON `app_id_to_user_id` (`user_id`); - -CREATE TABLE `all_auth_recipe_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `primary_or_recipe_user_id` char(36) NOT NULL, - `is_linked_or_is_a_primary_user` BOOLEAN NOT NULL DEFAULT FALSE, - `recipe_id` varchar(128) NOT NULL, - `time_joined` bigint unsigned NOT NULL, - `primary_or_recipe_user_time_joined` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`), - KEY `app_id` (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE, - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE, - FOREIGN KEY (`app_id`, `primary_or_recipe_user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users - (app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users - (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users - (primary_or_recipe_user_id, app_id); - -CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users - (app_id, recipe_id, tenant_id); - -CREATE TABLE `userid_mapping` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `supertokens_user_id` char(36) NOT NULL, - `external_user_id` varchar(128) NOT NULL, - `external_user_id_info` text, - PRIMARY KEY (`app_id`,`supertokens_user_id`,`external_user_id`), - UNIQUE KEY `supertokens_user_id` (`app_id`,`supertokens_user_id`), - UNIQUE KEY `external_user_id` (`app_id`,`external_user_id`), - FOREIGN KEY (`app_id`, `supertokens_user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `dashboard_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `email` varchar(256) NOT NULL, - `password_hash` varchar(256) NOT NULL, - `time_joined` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - UNIQUE KEY `email` (`app_id`,`email`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `dashboard_user_sessions` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `session_id` char(36) NOT NULL, - `user_id` char(36) NOT NULL, - `time_created` bigint unsigned NOT NULL, - `expiry` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`session_id`), - KEY `app_id` (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `dashboard_users` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE INDEX `dashboard_user_sessions_expiry_index` ON `dashboard_user_sessions` (`expiry`); - -CREATE TABLE `session_access_token_signing_keys` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `created_at_time` bigint unsigned NOT NULL, - `value` text, - PRIMARY KEY (`app_id`,`created_at_time`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `session_info` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `session_handle` varchar(255) NOT NULL, - `user_id` varchar(128) NOT NULL, - `refresh_token_hash_2` varchar(128) NOT NULL, - `session_data` text, - `expires_at` bigint unsigned NOT NULL, - `created_at_time` bigint unsigned NOT NULL, - `jwt_user_payload` text, - `use_static_key` tinyint(1) NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`session_handle`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE INDEX `session_expiry_index` ON `session_info` (`expires_at`); - -CREATE TABLE `user_last_active` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `last_active_time` bigint unsigned DEFAULT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `emailpassword_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `email` varchar(256) NOT NULL, - `password_hash` varchar(256) NOT NULL, - `time_joined` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `emailpassword_user_to_tenant` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `email` varchar(256) NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`), - UNIQUE KEY `email` (`app_id`,`tenant_id`,`email`), - FOREIGN KEY (`app_id`, `tenant_id`, `user_id`) REFERENCES `all_auth_recipe_users` (`app_id`, `tenant_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `emailpassword_pswd_reset_tokens` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `token` varchar(128) NOT NULL, - `token_expiry` bigint unsigned NOT NULL, - `email` varchar(256), - PRIMARY KEY (`app_id`,`user_id`,`token`), - UNIQUE KEY `token` (`token`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE ON UPDATE CASCADE -); - -CREATE INDEX `emailpassword_password_reset_token_expiry_index` ON `emailpassword_pswd_reset_tokens` (`token_expiry`); - -CREATE TABLE `emailverification_verified_emails` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `email` varchar(256) NOT NULL, - PRIMARY KEY (`app_id`,`user_id`,`email`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `emailverification_tokens` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `email` varchar(256) NOT NULL, - `token` varchar(128) NOT NULL, - `token_expiry` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`,`email`,`token`), - UNIQUE KEY `token` (`token`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE INDEX `emailverification_tokens_index` ON `emailverification_tokens` (`token_expiry`); - -CREATE TABLE `thirdparty_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `third_party_id` varchar(28) NOT NULL, - `third_party_user_id` varchar(256) NOT NULL, - `user_id` char(36) NOT NULL, - `email` varchar(256) NOT NULL, - `time_joined` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE INDEX `thirdparty_users_email_index` ON `thirdparty_users` (`app_id`,`email`); - -CREATE INDEX `thirdparty_users_thirdparty_user_id_index` ON `thirdparty_users` (`app_id`,`third_party_id`,`third_party_user_id`); - -CREATE TABLE `thirdparty_user_to_tenant` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `third_party_id` varchar(28) NOT NULL, - `third_party_user_id` varchar(256) NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`), - UNIQUE KEY `third_party_user_id` (`app_id`,`tenant_id`,`third_party_id`,`third_party_user_id`), - FOREIGN KEY (`app_id`, `tenant_id`, `user_id`) REFERENCES `all_auth_recipe_users` (`app_id`, `tenant_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `passwordless_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `email` varchar(256) DEFAULT NULL, - `phone_number` varchar(256) DEFAULT NULL, - `time_joined` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `app_id_to_user_id` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `passwordless_user_to_tenant` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` char(36) NOT NULL, - `email` varchar(256) DEFAULT NULL, - `phone_number` varchar(256) DEFAULT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`), - UNIQUE KEY `email` (`app_id`,`tenant_id`,`email`), - UNIQUE KEY `phone_number` (`app_id`,`tenant_id`,`phone_number`), - FOREIGN KEY (`app_id`, `tenant_id`, `user_id`) REFERENCES `all_auth_recipe_users` (`app_id`, `tenant_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `passwordless_devices` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `device_id_hash` char(44) NOT NULL, - `email` varchar(256) DEFAULT NULL, - `phone_number` varchar(256) DEFAULT NULL, - `link_code_salt` char(44) NOT NULL, - `failed_attempts` int unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`device_id_hash`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE INDEX `passwordless_devices_email_index` ON `passwordless_devices` (`app_id`,`tenant_id`,`email`); - -CREATE INDEX `passwordless_devices_phone_number_index` ON `passwordless_devices` (`app_id`,`tenant_id`,`phone_number`); - -CREATE TABLE `passwordless_codes` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `code_id` char(36) NOT NULL, - `device_id_hash` char(44) NOT NULL, - `link_code_hash` char(44) NOT NULL, - `created_at` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`code_id`), - UNIQUE KEY `link_code_hash` (`app_id`,`tenant_id`,`link_code_hash`), - KEY `app_id` (`app_id`,`tenant_id`,`device_id_hash`), - FOREIGN KEY (`app_id`, `tenant_id`, `device_id_hash`) REFERENCES `passwordless_devices` (`app_id`, `tenant_id`, `device_id_hash`) ON DELETE CASCADE ON UPDATE CASCADE -); - -CREATE INDEX `passwordless_codes_created_at_index` ON `passwordless_codes` (`app_id`,`tenant_id`,`created_at`); - -CREATE TABLE `jwt_signing_keys` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `key_id` varchar(255) NOT NULL, - `key_string` text NOT NULL, - `algorithm` varchar(10) NOT NULL, - `created_at` bigint unsigned DEFAULT NULL, - PRIMARY KEY (`app_id`,`key_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `user_metadata` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `user_metadata` text NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `roles` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `role` varchar(255) NOT NULL, - PRIMARY KEY (`app_id`,`role`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `role_permissions` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `role` varchar(255) NOT NULL, - `permission` varchar(255) NOT NULL, - PRIMARY KEY (`app_id`,`role`,`permission`), - FOREIGN KEY (`app_id`, `role`) REFERENCES `roles` (`app_id`, `role`) ON DELETE CASCADE -); - -CREATE INDEX `role_permissions_permission_index` ON `role_permissions` (`app_id`,`permission`); - -CREATE TABLE `user_roles` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `role` varchar(255) NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`,`role`), - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE INDEX `user_roles_role_index` ON `user_roles` (`app_id`,`tenant_id`,`role`); - -CREATE TABLE `totp_users` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - PRIMARY KEY (`app_id`,`user_id`), - FOREIGN KEY (`app_id`) REFERENCES `apps` (`app_id`) ON DELETE CASCADE -); - -CREATE TABLE `totp_user_devices` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `device_name` varchar(256) NOT NULL, - `secret_key` varchar(256) NOT NULL, - `period` int NOT NULL, - `skew` int NOT NULL, - `verified` tinyint(1) NOT NULL, - `created_at` BIGINT UNSIGNED, - PRIMARY KEY (`app_id`,`user_id`,`device_name`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `totp_users` (`app_id`, `user_id`) ON DELETE CASCADE -); - -CREATE TABLE `totp_used_codes` ( - `app_id` varchar(64) NOT NULL DEFAULT 'public', - `tenant_id` varchar(64) NOT NULL DEFAULT 'public', - `user_id` varchar(128) NOT NULL, - `code` varchar(8) NOT NULL, - `is_valid` tinyint(1) NOT NULL, - `expiry_time_ms` bigint unsigned NOT NULL, - `created_time_ms` bigint unsigned NOT NULL, - PRIMARY KEY (`app_id`,`tenant_id`,`user_id`,`created_time_ms`), - KEY `app_id` (`app_id`,`user_id`), - FOREIGN KEY (`app_id`, `user_id`) REFERENCES `totp_users` (`app_id`, `user_id`) ON DELETE CASCADE, - FOREIGN KEY (`app_id`, `tenant_id`) REFERENCES `tenants` (`app_id`, `tenant_id`) ON DELETE CASCADE -); - -CREATE INDEX `totp_used_codes_expiry_time_ms_index` ON `totp_used_codes` (`app_id`,`tenant_id`,`expiry_time_ms`); - -CREATE TABLE IF NOT EXISTS bulk_import_users ( - id CHAR(36), - app_id VARCHAR(64) NOT NULL DEFAULT 'public', - primary_user_id VARCHAR(36), - raw_data TEXT NOT NULL, - status VARCHAR(128) DEFAULT 'NEW', - error_msg TEXT, - created_at BIGINT UNSIGNED NOT NULL, - updated_at BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (app_id, id), - FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX bulk_import_users_status_updated_at_index ON bulk_import_users (app_id, status, updated_at); - -CREATE INDEX bulk_import_users_pagination_index1 ON bulk_import_users (app_id, status, created_at DESC, id DESC); - -CREATE INDEX bulk_import_users_pagination_index2 ON bulk_import_users (app_id, created_at DESC, id DESC); - -CREATE INDEX session_info_user_id_app_id_index ON session_info (user_id, app_id); -``` - - ## PostgreSQL tables - -```sql -CREATE TABLE apps ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT, - CONSTRAINT apps_pkey PRIMARY KEY (app_id) -); - -CREATE TABLE tenants ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT, - CONSTRAINT tenants_pkey PRIMARY KEY (app_id, tenant_id), - CONSTRAINT tenants_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX tenants_app_id_index ON tenants (app_id); - -CREATE TABLE tenant_configs ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - core_config TEXT, - email_password_enabled BOOLEAN, - passwordless_enabled BOOLEAN, - third_party_enabled BOOLEAN, - is_first_factors_null BOOLEAN, - CONSTRAINT tenant_configs_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id) -); - -CREATE TABLE tenant_thirdparty_providers ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - name VARCHAR(64), - authorization_endpoint TEXT, - authorization_endpoint_query_params TEXT, - token_endpoint TEXT, - token_endpoint_body_params TEXT, - user_info_endpoint TEXT, - user_info_endpoint_query_params TEXT, - user_info_endpoint_headers TEXT, - jwks_uri TEXT, - oidc_discovery_endpoint TEXT, - require_email BOOLEAN, - user_info_map_from_id_token_payload_user_id VARCHAR(64), - user_info_map_from_id_token_payload_email VARCHAR(64), - user_info_map_from_id_token_payload_email_verified VARCHAR(64), - user_info_map_from_user_info_endpoint_user_id VARCHAR(64), - user_info_map_from_user_info_endpoint_email VARCHAR(64), - user_info_map_from_user_info_endpoint_email_verified VARCHAR(64), - CONSTRAINT tenant_thirdparty_providers_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id), - CONSTRAINT tenant_thirdparty_providers_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX tenant_thirdparty_providers_tenant_id_index ON tenant_thirdparty_providers (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE tenant_thirdparty_provider_clients ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - client_type VARCHAR(64) DEFAULT '' NOT NULL, - client_id VARCHAR(256) NOT NULL, - client_secret TEXT, - scope VARCHAR(128)[], - force_pkce BOOLEAN, - additional_config TEXT, - CONSTRAINT tenant_thirdparty_provider_clients_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, third_party_id, client_type), - CONSTRAINT tenant_thirdparty_provider_clients_third_party_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id, third_party_id) REFERENCES public.tenant_thirdparty_providers(connection_uri_domain, app_id, tenant_id, third_party_id) ON DELETE CASCADE -); - -CREATE INDEX tenant_thirdparty_provider_clients_third_party_id_index ON tenant_thirdparty_provider_clients (connection_uri_domain, app_id, tenant_id, third_party_id); - -CREATE TABLE tenant_first_factors ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - factor_id VARCHAR(128), - CONSTRAINT tenant_first_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), - CONSTRAINT tenant_first_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS tenant_first_factors_tenant_id_index ON tenant_first_factors (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE tenant_required_secondary_factors ( - connection_uri_domain VARCHAR(256) DEFAULT '' NOT NULL, - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - factor_id VARCHAR(128), - CONSTRAINT tenant_required_secondary_factors_pkey PRIMARY KEY (connection_uri_domain, app_id, tenant_id, factor_id), - CONSTRAINT tenant_required_secondary_factors_tenant_id_fkey FOREIGN KEY (connection_uri_domain, app_id, tenant_id) REFERENCES public.tenant_configs(connection_uri_domain, app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS tenant_default_required_factor_ids_tenant_id_index ON tenant_required_secondary_factors (connection_uri_domain, app_id, tenant_id); - -CREATE TABLE key_value ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - name VARCHAR(128) NOT NULL, - value TEXT, - created_at_time BIGINT, - CONSTRAINT key_value_pkey PRIMARY KEY (app_id, tenant_id, name), - CONSTRAINT key_value_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX key_value_tenant_id_index ON key_value (app_id, tenant_id); - -CREATE TABLE app_id_to_user_id ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - recipe_id VARCHAR(128) NOT NULL, - primary_or_recipe_user_id CHAR(36) NOT NULL, - is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, - CONSTRAINT app_id_to_user_id_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT app_id_to_user_id_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, - CONSTRAINT app_id_to_user_id_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX app_id_to_user_id_app_id_index ON app_id_to_user_id (app_id); - -CREATE INDEX app_id_to_user_id_primary_user_id_index ON app_id_to_user_id (primary_or_recipe_user_id, app_id); - -CREATE TABLE all_auth_recipe_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - primary_or_recipe_user_id CHAR(36) NOT NULL, - is_linked_or_is_a_primary_user BOOLEAN NOT NULL DEFAULT FALSE, - recipe_id VARCHAR(128) NOT NULL, - time_joined BIGINT NOT NULL, - primary_or_recipe_user_time_joined BIGINT NOT NULL, - CONSTRAINT all_auth_recipe_users_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT all_auth_recipe_users_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, - CONSTRAINT all_auth_recipe_users_primary_or_recipe_user_id_fkey FOREIGN KEY(app_id, primary_or_recipe_user_id) REFERENCES public.app_id_to_user_id (app_id, user_id) ON DELETE CASCADE, - CONSTRAINT all_auth_recipe_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX all_auth_recipe_users_pagination_index1 ON all_auth_recipe_users - (app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_pagination_index2 ON all_auth_recipe_users - (app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); - -CREATE INDEX all_auth_recipe_users_pagination_index3 ON all_auth_recipe_users - (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC); - - -CREATE INDEX all_auth_recipe_users_pagination_index4 ON all_auth_recipe_users - (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined ASC, primary_or_recipe_user_id DESC); - - -CREATE INDEX all_auth_recipe_users_primary_user_id_index ON all_auth_recipe_users - (primary_or_recipe_user_id, app_id); - -CREATE INDEX all_auth_recipe_users_recipe_id_index ON all_auth_recipe_users - (app_id, recipe_id, tenant_id); - -CREATE INDEX all_auth_recipe_user_id_index ON all_auth_recipe_users (app_id, user_id); - -CREATE INDEX all_auth_recipe_tenant_id_index ON all_auth_recipe_users (app_id, tenant_id); - -CREATE TABLE userid_mapping ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - supertokens_user_id character(36) NOT NULL, - external_user_id VARCHAR(128) NOT NULL, - external_user_id_info TEXT, - CONSTRAINT userid_mapping_external_user_id_key UNIQUE (app_id, external_user_id), - CONSTRAINT userid_mapping_pkey PRIMARY KEY (app_id, supertokens_user_id, external_user_id), - CONSTRAINT userid_mapping_supertokens_user_id_key UNIQUE (app_id, supertokens_user_id), - CONSTRAINT userid_mapping_supertokens_user_id_fkey FOREIGN KEY (app_id, supertokens_user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX userid_mapping_supertokens_user_id_index ON userid_mapping (app_id, supertokens_user_id); - -CREATE TABLE dashboard_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - password_hash VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT dashboard_users_email_key UNIQUE (app_id, email), - CONSTRAINT dashboard_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT dashboard_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX dashboard_users_app_id_index ON dashboard_users (app_id); - -CREATE TABLE dashboard_user_sessions ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - session_id character(36) NOT NULL, - user_id character(36) NOT NULL, - time_created BIGINT NOT NULL, - expiry BIGINT NOT NULL, - CONSTRAINT dashboard_user_sessions_pkey PRIMARY KEY (app_id, session_id), - CONSTRAINT dashboard_user_sessions_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.dashboard_users(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX dashboard_user_sessions_expiry_index ON dashboard_user_sessions (expiry); - -CREATE INDEX dashboard_user_sessions_user_id_index ON dashboard_user_sessions (app_id, user_id); - -CREATE TABLE session_access_token_signing_keys ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - created_at_time BIGINT NOT NULL, - value TEXT, - CONSTRAINT session_access_token_signing_keys_pkey PRIMARY KEY (app_id, created_at_time), - CONSTRAINT session_access_token_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX access_token_signing_keys_app_id_index ON session_access_token_signing_keys (app_id); - -CREATE TABLE session_info ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - session_handle VARCHAR(255) NOT NULL, - user_id VARCHAR(128) NOT NULL, - refresh_token_hash_2 VARCHAR(128) NOT NULL, - session_data TEXT, - expires_at BIGINT NOT NULL, - created_at_time BIGINT NOT NULL, - jwt_user_payload TEXT, - use_static_key BOOLEAN NOT NULL, - CONSTRAINT session_info_pkey PRIMARY KEY (app_id, tenant_id, session_handle), - CONSTRAINT session_info_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX session_expiry_index ON session_info (expires_at); - -CREATE INDEX session_info_tenant_id_index ON session_info (app_id, tenant_id); - -CREATE TABLE user_last_active ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - last_active_time BIGINT, - CONSTRAINT user_last_active_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT user_last_active_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX user_last_active_app_id_index ON user_last_active (app_id); - -CREATE TABLE emailpassword_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - password_hash VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT emailpassword_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT emailpassword_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE emailpassword_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - CONSTRAINT emailpassword_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), - CONSTRAINT emailpassword_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT emailpassword_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE emailpassword_pswd_reset_tokens ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - token VARCHAR(128) NOT NULL, - token_expiry BIGINT NOT NULL, - email VARCHAR(256), - CONSTRAINT emailpassword_pswd_reset_tokens_pkey PRIMARY KEY (app_id, user_id, token), - CONSTRAINT emailpassword_pswd_reset_tokens_token_key UNIQUE (token), - CONSTRAINT emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX emailpassword_password_reset_token_expiry_index ON emailpassword_pswd_reset_tokens (token_expiry); - -CREATE INDEX emailpassword_pswd_reset_tokens_user_id_index ON emailpassword_pswd_reset_tokens (app_id, user_id); - -CREATE TABLE emailverification_verified_emails ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - email VARCHAR(256) NOT NULL, - CONSTRAINT emailverification_verified_emails_pkey PRIMARY KEY (app_id, user_id, email), - CONSTRAINT emailverification_verified_emails_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX emailverification_verified_emails_app_id_index ON emailverification_verified_emails (app_id); - -CREATE TABLE emailverification_tokens ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - email VARCHAR(256) NOT NULL, - token VARCHAR(128) NOT NULL, - token_expiry BIGINT NOT NULL, - CONSTRAINT emailverification_tokens_pkey PRIMARY KEY (app_id, tenant_id, user_id, email, token), - CONSTRAINT emailverification_tokens_token_key UNIQUE (token), - CONSTRAINT emailverification_tokens_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX emailverification_tokens_index ON emailverification_tokens (token_expiry); - -CREATE INDEX emailverification_tokens_tenant_id_index ON emailverification_tokens (app_id, tenant_id); - -CREATE TABLE thirdparty_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - third_party_user_id VARCHAR(256) NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256) NOT NULL, - time_joined BIGINT NOT NULL, - CONSTRAINT thirdparty_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT thirdparty_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX thirdparty_users_email_index ON thirdparty_users (app_id, email); - -CREATE INDEX thirdparty_users_thirdparty_user_id_index ON thirdparty_users (app_id, third_party_id, third_party_user_id); - -CREATE TABLE thirdparty_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - third_party_id VARCHAR(28) NOT NULL, - third_party_user_id VARCHAR(256) NOT NULL, - CONSTRAINT thirdparty_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT thirdparty_user_to_tenant_third_party_user_id_key UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id), - CONSTRAINT thirdparty_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - time_joined BIGINT NOT NULL, - CONSTRAINT passwordless_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT passwordless_users_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.app_id_to_user_id(app_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_user_to_tenant ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id character(36) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - CONSTRAINT passwordless_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email), - CONSTRAINT passwordless_user_to_tenant_phone_number_key UNIQUE (app_id, tenant_id, phone_number), - CONSTRAINT passwordless_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id), - CONSTRAINT passwordless_user_to_tenant_user_id_fkey FOREIGN KEY (app_id, tenant_id, user_id) REFERENCES public.all_auth_recipe_users(app_id, tenant_id, user_id) ON DELETE CASCADE -); - -CREATE TABLE passwordless_devices ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - device_id_hash character(44) NOT NULL, - email VARCHAR(256), - phone_number VARCHAR(256), - link_code_salt character(44) NOT NULL, - failed_attempts integer NOT NULL, - CONSTRAINT passwordless_devices_pkey PRIMARY KEY (app_id, tenant_id, device_id_hash), - CONSTRAINT passwordless_devices_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX passwordless_devices_email_index ON passwordless_devices (app_id, tenant_id, email); - -CREATE INDEX passwordless_devices_phone_number_index ON passwordless_devices (app_id, tenant_id, phone_number); - -CREATE INDEX passwordless_devices_tenant_id_index ON passwordless_devices (app_id, tenant_id); - -CREATE TABLE passwordless_codes ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - code_id character(36) NOT NULL, - device_id_hash character(44) NOT NULL, - link_code_hash character(44) NOT NULL, - created_at BIGINT NOT NULL, - CONSTRAINT passwordless_codes_link_code_hash_key UNIQUE (app_id, tenant_id, link_code_hash), - CONSTRAINT passwordless_codes_pkey PRIMARY KEY (app_id, tenant_id, code_id), - CONSTRAINT passwordless_codes_device_id_hash_fkey FOREIGN KEY (app_id, tenant_id, device_id_hash) REFERENCES public.passwordless_devices(app_id, tenant_id, device_id_hash) ON UPDATE CASCADE ON DELETE CASCADE -); - -CREATE INDEX passwordless_codes_created_at_index ON passwordless_codes (app_id, tenant_id, created_at); - -CREATE INDEX passwordless_codes_device_id_hash_index ON passwordless_codes (app_id, tenant_id, device_id_hash); - -CREATE TABLE jwt_signing_keys ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - key_id VARCHAR(255) NOT NULL, - key_string TEXT NOT NULL, - algorithm VARCHAR(10) NOT NULL, - created_at BIGINT, - CONSTRAINT jwt_signing_keys_pkey PRIMARY KEY (app_id, key_id), - CONSTRAINT jwt_signing_keys_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX jwt_signing_keys_app_id_index ON jwt_signing_keys (app_id); - -CREATE TABLE user_metadata ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - user_metadata TEXT NOT NULL, - CONSTRAINT user_metadata_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT user_metadata_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX user_metadata_app_id_index ON user_metadata (app_id); - -CREATE TABLE roles ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - role VARCHAR(255) NOT NULL, - CONSTRAINT roles_pkey PRIMARY KEY (app_id, role), - CONSTRAINT roles_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX roles_app_id_index ON roles (app_id); - -CREATE TABLE role_permissions ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - role VARCHAR(255) NOT NULL, - permission VARCHAR(255) NOT NULL, - CONSTRAINT role_permissions_pkey PRIMARY KEY (app_id, role, permission), - CONSTRAINT role_permissions_role_fkey FOREIGN KEY (app_id, role) REFERENCES public.roles(app_id, role) ON DELETE CASCADE -); - -CREATE INDEX role_permissions_permission_index ON role_permissions (app_id, permission); - -CREATE INDEX role_permissions_role_index ON role_permissions (app_id, role); - -CREATE TABLE user_roles ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - role VARCHAR(255) NOT NULL, - CONSTRAINT user_roles_pkey PRIMARY KEY (app_id, tenant_id, user_id, role), - CONSTRAINT user_roles_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE -); - -CREATE INDEX user_roles_role_index ON user_roles (app_id, tenant_id, role); - -CREATE INDEX user_roles_tenant_id_index ON user_roles (app_id, tenant_id); - -CREATE INDEX user_roles_app_id_role_index ON user_roles (app_id, role); - -CREATE TABLE totp_users ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - CONSTRAINT totp_users_pkey PRIMARY KEY (app_id, user_id), - CONSTRAINT totp_users_app_id_fkey FOREIGN KEY (app_id) REFERENCES public.apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX totp_users_app_id_index ON totp_users (app_id); - -CREATE TABLE totp_user_devices ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - device_name VARCHAR(256) NOT NULL, - secret_key VARCHAR(256) NOT NULL, - period integer NOT NULL, - skew integer NOT NULL, - verified BOOLEAN NOT NULL, - created_at BIGINT, - CONSTRAINT totp_user_devices_pkey PRIMARY KEY (app_id, user_id, device_name), - CONSTRAINT totp_user_devices_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX totp_user_devices_user_id_index ON totp_user_devices (app_id, user_id); - -CREATE TABLE totp_used_codes ( - app_id VARCHAR(64) DEFAULT 'public' NOT NULL, - tenant_id VARCHAR(64) DEFAULT 'public' NOT NULL, - user_id VARCHAR(128) NOT NULL, - code VARCHAR(8) NOT NULL, - is_valid BOOLEAN NOT NULL, - expiry_time_ms BIGINT NOT NULL, - created_time_ms BIGINT NOT NULL, - CONSTRAINT totp_used_codes_pkey PRIMARY KEY (app_id, tenant_id, user_id, created_time_ms), - CONSTRAINT totp_used_codes_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES public.tenants(app_id, tenant_id) ON DELETE CASCADE, - CONSTRAINT totp_used_codes_user_id_fkey FOREIGN KEY (app_id, user_id) REFERENCES public.totp_users(app_id, user_id) ON DELETE CASCADE -); - -CREATE INDEX totp_used_codes_expiry_time_ms_index ON totp_used_codes (app_id, tenant_id, expiry_time_ms); - -CREATE INDEX totp_used_codes_tenant_id_index ON totp_used_codes (app_id, tenant_id); - -CREATE INDEX totp_used_codes_user_id_index ON totp_used_codes (app_id, user_id); - -CREATE TABLE IF NOT EXISTS bulk_import_users ( - id CHAR(36), - app_id VARCHAR(64) NOT NULL DEFAULT 'public', - primary_user_id VARCHAR(36), - raw_data TEXT NOT NULL, - status VARCHAR(128) DEFAULT 'NEW', - error_msg TEXT, - created_at BIGINT NOT NULL, - updated_at BIGINT NOT NULL, - CONSTRAINT bulk_import_users_pkey PRIMARY KEY(app_id, id), - CONSTRAINT bulk_import_users__app_id_fkey FOREIGN KEY(app_id) REFERENCES apps(app_id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS bulk_import_users_status_updated_at_index ON bulk_import_users (app_id, status, updated_at); - -CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index1 ON bulk_import_users (app_id, status, created_at DESC, id DESC); - -CREATE INDEX IF NOT EXISTS bulk_import_users_pagination_index2 ON bulk_import_users (app_id, created_at DESC, id DESC); - -CREATE INDEX IF NOT EXISTS session_info_user_id_app_id_index ON session_info (user_id, app_id); -``` - - - +Core creates and migrates its required tables automatically when the database principal has DDL permission. The previous +manual SQL on this page was incomplete across Core 10.x releases and has been removed. If the runtime principal cannot +perform DDL, use a schema or migration artifact generated and tested for the exact Core 10.x release you deploy; do not +reuse a schema copied from another patch or minor release. ::: -#### 4.3 Test the connection +#### 4.3 Test the connection -To test, start SuperTokens and run the following query in your database +Start the exact Core 10.x release against a staging copy of the database and require startup/migration success. Then +exercise an authenticated SDK operation. Querying one table does not prove that all release-matched migrations exist. -```sql -SELECT * FROM key_value; -``` -If you see at least one row, it means that the connection has been successfully completed! +#### 4.4 Rename database tables (optional) -#### 4.4 Rename database tables {{optional}} - -:::caution -If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one. +:::warning[If you already have tables created by SuperTokens, and then you rename them, SuperTokens creates new tables. Please be sure to migrate the data from the existing one to the new one.] ::: -You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. +You can add a prefix to all table names that SuperTokens manages. This way, all will be renamed in a way that has no clashes with your tables. For example, two tables created by SuperTokens have the names `emailpassword_users` and `thirdparty_users`. If you add a prefix to them (something like `"my_prefix"`), then the tables become `my_prefix_emailpassword_users` and `my_prefix_thirdparty_users`. ##### For MySQL - - - + + ```bash docker run \ - -p 3567:3567 \ - // highlight-next-line - -e MYSQL_TABLE_NAMES_PREFIX="my_prefix" \ - -d supertokens/supertokens-mysql + --network app-network \ + -e MYSQL_TABLE_NAMES_PREFIX="my_prefix" \ + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-mysql:10.1 ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command mysql_table_names_prefix: "my_prefix" ``` - - + + ##### For PostgreSQL - - - + + ```bash docker run \ - -p 3567:3567 \ - // highlight-next-line - -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ - -d supertokens/supertokens-postgresql + --network app-network \ + -e POSTGRESQL_TABLE_NAMES_PREFIX="my_prefix" \ + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d supertokens/supertokens-postgresql:10.1 ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command postgresql_table_names_prefix: "my_prefix" ``` - - + + ### 5. Add license keys -To access some features in your self-hosted service you must use **license keys**. -You can sign up on [**SuperTokens**](https://supertokens.com/auth) to receive one. +Some Core v10 enterprise features require a license key. Confirm with SuperTokens that your plan and key apply to the +exact legacy release; this archived page does not establish the current acquisition process. Once you have the license key you need to manually add it to your **SuperTokens Core Instance**. To do this you have to call the Core API with the following request: -```bash title="Add License Key" showAppTypeSelect -curl --location --request PUT ^{coreInfo.uri}/ee/license \ +```bash title="Add License Key" +curl --location --request PUT "${CORE_API_ENDPOINT:?Set the private Core endpoint}/ee/license" \ --header 'Content-Type: application/json' \ - --header 'api-key: ^{coreInfo.key}' \ - --data-raw '{ "licenseKey": "" }' + --header "api-key: ${SUPERTOKENS_API_KEY:?Set the Core API key}" \ + --data-raw "{ \"licenseKey\": \"${SUPERTOKENS_LICENSE_KEY:?Set the license key}\" }" ``` -## References - -### Docker compose file - - - - -```bash -version: '3' - -services: - db: - image: mysql:latest - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_USER: supertokens_user - MYSQL_PASSWORD: somePassword - MYSQL_DATABASE: supertokens - ports: - - 3306:3306 - networks: - - app_network - restart: unless-stopped - healthcheck: - test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] - timeout: 20s - retries: 10 - - supertokens: - image: supertokens/supertokens-mysql@10.1 - depends_on: - db: - condition: service_healthy - ports: - - 3567:3567 - environment: - MYSQL_CONNECTION_URI: mysql://supertokens_user:somePassword@db:3306/supertokens - networks: - - app_network - restart: unless-stopped - healthcheck: - test: > - bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"' - interval: 10s - timeout: 5s - retries: 5 - -networks: - app_network: - driver: bridge -``` - - - - - -```bash -version: '3' - -services: - # Note: If you are assigning a custom name to your db service on the line below, make sure it does not contain underscores - db: - image: 'postgres:latest' - environment: - POSTGRES_USER: supertokens_user - POSTGRES_PASSWORD: somePassword - POSTGRES_DB: supertokens - ports: - - 5432:5432 - networks: - - app_network - restart: unless-stopped - healthcheck: - test: ['CMD', 'pg_isready', '-U', 'supertokens_user', '-d', 'supertokens'] - interval: 5s - timeout: 5s - retries: 5 - - supertokens: - image: supertokens/supertokens-postgresql^{docker_version_postgresql} - depends_on: - db: - condition: service_healthy - ports: - - 3567:3567 - environment: - POSTGRESQL_CONNECTION_URI: "postgresql://supertokens_user:somePassword@db:5432/supertokens" - networks: - - app_network - restart: unless-stopped - healthcheck: - test: > - bash -c 'exec 3<>/dev/tcp/127.0.0.1/3567 && echo -e "GET /hello HTTP/1.1\r\nhost: 127.0.0.1:3567\r\nConnection: close\r\n\r\n" >&3 && cat <&3 | grep "Hello"' - interval: 10s - timeout: 5s - retries: 5 - -networks: - app_network: - driver: bridge -``` - - - - - -:::important -If you are running the backend process that integrates with the backend SDK as part of the docker compose file as well, make sure to use `http://supertokens:3567` as the connection URI instead of `http://localhost:3567`. -::: - -### Helm charts for Kubernetes - -- For [MySQL image](https://github.com/supertokens/supertokens-docker-mysql/tree/master/helm-chart) +## Select immutable legacy artifacts -- For [PostgreSQL image](https://github.com/supertokens/supertokens-docker-postgresql/tree/master/helm-chart) +The Core images on this page are pinned to the released v10.1 image line so that Docker does not silently select a newer +Core major. For a production archive, resolve those tags to registry digests and record +`repository:10.1@sha256:` in deployment configuration. Pin the database image by digest from a version supported +by your verified Core v10/database-plugin tuple, scan all images, and promote unchanged digests between environments. +The archived guide does not establish an immutable MySQL/PostgreSQL image mapping or a maintained Helm-chart release. +Consequently, the old mutable Compose examples and deprecated Helm links have been removed. Confirm the database support +matrix and exact artifacts before treating this legacy guide as a reproducible production deployment. diff --git a/docs/migration/_blocks/add-users-for-bulk-import-request.mdx b/docs/migration/_blocks/add-users-for-bulk-import-request.mdx deleted file mode 100644 index 44ca9784eb..0000000000 --- a/docs/migration/_blocks/add-users-for-bulk-import-request.mdx +++ /dev/null @@ -1,195 +0,0 @@ - - - - Stages users to be imported by a background cron job. - - - **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. - - - - - -

Request

- -

Body Schema

- -| Name | Type | Description | Required | -|----------------|---------------------|-----------------------------------|----------| -| users | `Array` of `User` objects | The users that you want to import. The array has a limit of 10000 items. | Yes | - - -

User

- -| Name | Type | Description | Required | -|----------------|------------------------|-------------------------------------------------------------------------------------------------|----------| -| externalUserId | `string` | ID that can be used to reference the users from your previous provider | No | -| `userMetadata` | `object` | An object with custom user information that can be used later on | No | -| `userRoles` | `array` of `UserRole` | The roles that will be used for authorization | No | -| `totpDevices` | `array` of `TotpDevice` | Time-based One-time Password device (TOTP) used for Multi-Factor Authentication | Yes | -| `loginMethods` | `array` of `LoginMethod` | The actual authentication methods and credentials | Yes | - -##### TotpDevice - -| Name | Type | Description | Required | -|------------|---------------------|-----------------------------------------------------------------------------|----------| -| `secretKey` | `string` | The secret key used to generate the TOTP codes. | Yes | -| period | `number` | The time period in seconds for which a TOTP code is valid. | Yes | -| skew | `number` | The allowable time skew to account for clock differences between the server and device. | Yes | -| `deviceName` | `string` | The name assigned to the TOTP device for identification purposes. | No | - - -

UserRole

- -| Name | Type | Description | Required | -|------------|---------------------|-----------------------------------------------------------------------------|----------| -| role | `string` | The actual role name | Yes | -| tenantIds | Array of `string` | The tenants that use this role. If you are not using the `multi-tenancy` just pass an empty array. | Yes | - -##### LoginMethod - -`LoginMethod` is a polymorphic type with multiple variants based on the `recipeId`. Each variant includes shared and specific fields. - -###### EmailPassword - -###### With Encrypted Password - -| Name | Type | Description | Required | -|-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| -| email | `string` | User's email address | Yes | -| `passwordHash` | `string` | Hashed password | Yes | -| `hashingAlgorithm` | `enum` (`"bcrypt"`, `"argon2"`, `"firebase_scrypt"`) | Hashing algorithm used. | Yes | -| `recipeId` | `string` | Must be `emailpassword` | Yes | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -###### With Plain Password - -| Name | Type | Description | Required | -|-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| -| email | `string` | User's email address | Yes | -| `plaintextPassword` | `string` | A plain text password that will be hashed based on the configured hashing algorithm | Yes | -| `recipeId` | `string` | Must be `emailpassword` | Yes | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -##### ThirdParty - | Name | Type | Description | Required | - |------------------|--------|-------------------------------|----------| - | `recipeId` | `string` | Must be `"thirdparty"` | Yes | - | email | `string` | User's email address | Yes | - | thirdPartyId | `string` | Identifier for the third party provider | Yes | - | thirdPartyUserId | `string` | User identifier from the third party provider | Yes | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -##### Passwordless - - | Name | Type | Description | Required | - |----------|--------|-----------------------|----------| - | `recipeId` | `string` | Must be `"passwordless"` | Yes | - | email | `string` | User's email address | One of `email` or `phoneNumber` must be provided | - | `phoneNumber` | `string` | User's phone number | One of `email` or `phoneNumber` must be provided | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - - -

Example

- -```bash -curl --location --request POST '^{coreInfo.uri}/bulk-import/users' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - users: [ - { - "externalUserId": "user_2", - "userMetadata": { - "firstName": "John", - "lastName": "Doe", - "department": "Marketing" - }, - "userRoles": [{ "role": "editor", "tenantIds": [] }], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672617599000, - "recipeId": "thirdparty", - "email": "john.doe@gmail.com", - "thirdPartyId": "google", - "thirdPartyUserId": "google_987654321" - } - ] - } - ] -} -``` - -

Response

- -

200

-All the users have been added to the `bulk_import_users` table. - -

Example

- -```json -{ - "status": "OK", -} -``` - -

400

- -The request body was invalid. - -

Example

- -```json -```json -{ - error: "Failed to add users for the bulk import. Please fix the error in the following users", - users: [ - { - index: 11, - errors: ["No two loginMethods can have isPrimary as true."] - }, - { - index: 15, - errors: ["email is required for recipeId emailpassword", "hashingAlgorithm must be one of 'bcrypt', 'argon2', or 'firebase_scrypt'."] - } - ] -} -``` - -##### Errors - -- Account linking must be enabled if more than one login method is provided for a user. -- Multitenancy must be enabled if a `tenantId` is other than `public.` -- No two `loginMethods` can have `isPrimary` as `true`. -- Invalid `appId` or `tenantId`. -- A valid `email` is required. -- A valid `passwordHash` is required. -- `hashingAlgorithm` must be one of 'bcrypt', 'argon2', or `firebase_scrypt`. -- A valid `email` is required. -- `thirdPartyUserId` is required. -- A valid `email` or `phoneNumber` is required. - - -

500

- -An internal server error occurred. - -
diff --git a/docs/migration/_blocks/bulk-import-user-request.mdx b/docs/migration/_blocks/bulk-import-user-request.mdx deleted file mode 100644 index 06e2520106..0000000000 --- a/docs/migration/_blocks/bulk-import-user-request.mdx +++ /dev/null @@ -1,172 +0,0 @@ - - - - Creates one user at a time based on the request body. - - - - -**Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. - - - - - - -

Request

- -

Body Schema

- -| Name | Type | Description | Required | -|----------------|------------------------|-------------------------------------------------------------------------------------------------|----------| -| externalUserId | `string` | ID that can be used to reference the users from your previous provider | No | -| `userMetadata` | `object` | An object with custom user information that can be used later on | No | -| `userRoles` | `array` of `UserRole` | The roles that will be used for authorization | No | -| `totpDevices` | `array` of `TotpDevice` | Time-based One-time Password device (TOTP) used for Multi-Factor Authentication | Yes | -| `loginMethods` | `array` of `LoginMethod` | The actual authentication methods and credentials | Yes | - -

TotpDevice

- -| Name | Type | Description | Required | -|------------|---------------------|-----------------------------------------------------------------------------|----------| -| `secretKey` | `string` | The secret key used to generate the TOTP codes. | Yes | -| period | `number` | The time period in seconds for which a TOTP code is valid. | Yes | -| skew | `number` | The allowable time skew to account for clock differences between the server and device. | Yes | -| `deviceName` | `string` | The name assigned to the TOTP device for identification purposes. | No | - -

UserRole

- -| Name | Type | Description | Required | -|------------|---------------------|-----------------------------------------------------------------------------|----------| -| role | `string` | The actual role name | Yes | -| tenantIds | Array of `string` | The tenants that use this role. If you are not using the `multi-tenancy` just pass an empty array. | Yes | - -

LoginMethod

- -`LoginMethod` is a polymorphic type with multiple variants based on the `recipeId`. Each variant includes shared and specific fields. - -##### EmailPassword - -###### With Encrypted Password - -| Name | Type | Description | Required | -|-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| -| email | `string` | User's email address | Yes | -| `passwordHash` | `string` | Hashed password | Yes | -| `hashingAlgorithm` | `enum` (`"bcrypt"`, `"argon2"`, `"firebase_scrypt"`) | Hashing algorithm used. | Yes | -| `recipeId` | `string` | Must be `emailpassword` | Yes | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -###### With Plain Password - -| Name | Type | Description | Required | -|-------------------------|-----------------------|-------------------------------------------------------------------------------------------------|----------| -| email | `string` | User's email address | Yes | -| `plaintextPassword` | `string` | A plain text password that will be hashed based on the configured hashing algorithm | Yes | -| `recipeId` | `string` | Must be `emailpassword` | Yes | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - - -##### ThirdParty - | Name | Type | Description | Required | - |------------------|--------|-------------------------------|----------| - | `recipeId` | `string` | Must be `"thirdparty"` | Yes | - | email | `string` | User's email address | Yes | - | thirdPartyId | `string` | Identifier for the third party provider | Yes | - | thirdPartyUserId | `string` | User identifier from the third party provider | Yes | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -##### Passwordless - - | Name | Type | Description | Required | - |----------|--------|-----------------------|----------| - | `recipeId` | `string` | Must be `"passwordless"` | Yes | - | email | `string` | User's email address | One of `email` or `phoneNumber` must be provided | - | `phoneNumber` | `string` | User's phone number | One of `email` or `phoneNumber` must be provided | -| tenantIds | Array of `string` | The tenant IDs that the user belongs to (if you are using the `multi-tenancy` feature) | No | -| externalUserId | `string` | ID that can be used to reference the users from your previous provider only for this authentication method | No | -| `isVerified` | `boolean` | Indicates whether the user's email has been verified | No | -| `isPrimary` | `boolean` | Indicates whether this is the user's primary authentication method | No | -| timeJoinedInMSSinceEpoch| `number` | Timestamp representing when the user joined, in milliseconds since the Unix epoch | No | - -

Example

- -```bash -curl --location --request POST '^{coreInfo.uri}/bulk-import/import' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data ' - { - "externalUserId": "user_12345", - "userMetadata": { - "firstName": "Jane", - "lastName": "Doe", - "department": "Engineering" - }, - "userRoles": [{ "role": "admin", "tenantIds": [] }], - "totpDevices": [ - { - "secretKey": "JBSWY3DPEHPK3PXP", - "period": 30, - "skew": 1, - "deviceName": "Main Device" - } - ], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672531199000, - "recipeId": "emailpassword", - "email": "jane.doe@example.com", - "passwordHash": "$2b$12$KIXQeFz...", - "hashingAlgorithm": "bcrypt" - } - ] - } -' -``` - -

Response

- -

200

-The user has been successfully created. - -

Example

- -```json -{ - "status": "OK", - "user": { /* User Object */ } -} -``` - -

400

- -The request body was invalid. - -

Example

- -```json -{ - "errors": ["No two loginMethods can have isPrimary as true.", "email is required for recipeId emailpassword", "hashingAlgorithm must be one of 'bcrypt', 'argon2', or 'firebase_scrypt'."]} -} -``` - -

500

- -An internal server error occurred. - -
diff --git a/docs/migration/_blocks/count-staged-users-request.mdx b/docs/migration/_blocks/count-staged-users-request.mdx deleted file mode 100644 index df3433b2ba..0000000000 --- a/docs/migration/_blocks/count-staged-users-request.mdx +++ /dev/null @@ -1,53 +0,0 @@ - - - - Returns the total number of users that were staged for import. - The value can be filtered by status. - - - **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. - - - - - -

Request

- -

Query Parameters

- -| Name | Type | Description | Required | -|----------------|---------------------|-----------------------------------|----------| -| status | `enum` (`"NEW"`, `"PROCESSING"`, `"FAILED"`) | Can be used to count only users with a specific status. | No | - -

Example

- -```bash - curl --location --request GET '^{coreInfo.uri}/bulk-import/users/count?status=PROCESSING' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ -``` - -

Response

- -

200

- -The users have been successfully retrieved. - -

Example

- -```json -{ - "status": "OK", - "count": "", -} -``` - -

400

- -The tenant or the specified app have not been found. - -

500

- -An internal server error occurred. - -
diff --git a/docs/migration/_blocks/get-staged-users-request.mdx b/docs/migration/_blocks/get-staged-users-request.mdx deleted file mode 100644 index ff43ff4db0..0000000000 --- a/docs/migration/_blocks/get-staged-users-request.mdx +++ /dev/null @@ -1,61 +0,0 @@ - - - - Retrieves a paginated list of users that have been staged for import (from the `bulk_import_users` table). - - - **Authorization**: Set the `api-key` header to the value of your **SuperTokens** Core API key. - - - - - -

Request

- -

Query Parameters

- -| Name | Type | Description | Required | -|----------------|---------------------|-----------------------------------|----------| -| status | `enum` (`"NEW"`, `"PROCESSING"`, `"FAILED"`) | Can be used to filter the returned values. | No | -| limit | `string` | The number of results that will be return. Defaults to 100 | No | -| `paginationToken` | `string` | String token used to get a new chunk of data | No | - -

Example

- -```bash -curl --location --request GET '^{coreInfo.uri}/bulk-import/users?status=FAILED' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ -``` - -

Response

- -

User Schema

- -| Property | Type | Description | -|-------------|----------------------------------|-----------------------------------------------| -| id | `string` | Unique identifier for the user. | -| `raw_data` | `Object` | The initial data that you have added through the `/bulk-import/add-users` endpoint.| -| status | `string` (`"NEW"`, `"PROCESSING"`, `"FAILED"`) | Current status of the user. | -| `error_msg` | `Array` of `string` | List of error messages associated with the import process.| -| `created_at`| `string` (`date-time` format) | Timestamp when the user was created. | -| `updated_at`| `string` (`date-time` format) | Timestamp when the user was last updated. | - -

200

- -The users have been successfully retrieved. - -

Example

- -```json -{ - "users": [{ /* Array of Users */ }], - "nextPaginationToken": "" -} -``` - -

500

- -An internal server error occurred. - -
diff --git a/docs/migration/_category_.json b/docs/migration/_category_.json deleted file mode 100644 index 2b5ba9642a..0000000000 --- a/docs/migration/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Migration", - "collapsible": true, - "collapsed": true, - "customProps": { - "isMainCategory": true - }, - "position": 6 -} diff --git a/docs/migration/account-migration.mdx b/docs/migration/account-migration.mdx index 7a8fa40320..d67251dbb0 100644 --- a/docs/migration/account-migration.mdx +++ b/docs/migration/account-migration.mdx @@ -1,26 +1,14 @@ --- title: Account Migration -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 description: Migrate your users from a legacy authentication provider to SuperTokens. -page_type: tutorial -category: migration +sidebar: + order: 2 --- +The following guide shows you how to move users from your current authentication solution to **SuperTokens**. -import BulkImportUserRequest from "./_blocks/bulk-import-user-request.mdx" -import BulkImportUsersCountRequest from "./_blocks/count-staged-users-request.mdx" -import BulkImportUsersGetRequest from "./_blocks/get-staged-users-request.mdx" -import BulkImportUsersAddRequest from "./_blocks/add-users-for-bulk-import-request.mdx" - -# Account migration - - - The following guide will show you how to move your users from your current authentication solution to **SuperTokens** - +--- - ## Overview @@ -29,10 +17,10 @@ The process of migrating your accounts breaks down into two parts: ### Creating new users on the fly To ensure a smooth migration process, with no downtime, you need to be able to directly create new users from the legacy sign up flow. -This is necessary since there is a time gap between when you export all your data for bulk import and when you go live with **SuperTokens**. +This is necessary since there is a time gap between when you export all your data for bulk import and when you go live with **SuperTokens**. -New users might get created in that interval through your legacy authentication provider. -Hence, you also need to create them in **SuperTokens** to keep the data in sync. +New users might get created in that interval through your legacy authentication provider. +Hence, you also need to create them in **SuperTokens** to keep the data in sync. ### Adding most of your users through a bulk import @@ -41,370 +29,121 @@ This happens through the bulk import API. The process is asynchronous and can work with large amounts of data. - ## Before you start This guide assumes that you have already integrated **SuperTokens** with your existing stack. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction) and explore all the supported [authentication methods](/docs/authentication/overview). +If you have not, please check the [Quickstart Guide](/quickstart) and explore all the supported [authentication methods](/authentication/overview). + +Bulk import requires Core `10.0.0` or later and persistent database storage; the in-memory database does not support +these APIs. Before importing: + +- create and configure every target tenant, role, recipe, and third-party provider referenced by the import; +- enable account linking before importing a user with multiple login methods, and test your linking policy with a + representative export; +- decide how each legacy identity maps to a tenant and login method, and reject ambiguous or duplicate mappings; and +- take a restorable source export and define retry, reconciliation, rollback, and cutover procedures. + +For email/password users, provide either a supported `passwordHash` with its `hashingAlgorithm`, or a +`plainTextPassword`, as defined by the [bulk-import request schema](/references/cdi/bulk-import/addbulkimportusers). +Prefer compatible bcrypt, Argon2, or Firebase `scrypt` hashes over plain-text passwords. Treat exports, password hashes, +MFA secrets, API keys, and access tokens as credentials: encrypt them in transit and at rest, restrict access, never put +them in logs or user metadata, and securely delete temporary copies after reconciliation. ## Steps -### 1. Update the legacy sign up flow +### 1. Update the legacy sign up flow Modify the legacy sign up flow logic to also create new users in **SuperTokens**. You can do this through the `Import User` endpoint that allows you to directly create accounts. Call the endpoint from the authentication flow used by your legacy provider. - -## Auth0 Instructions - + -:::warning no-title + + +:::caution[Unverified mapping pseudocode] +The following Action illustrates where a login-time direct import can run. Its identity fields, provider mapping, and +`getPasswordHash` placeholder have not been validated against a current Auth0 password/MFA support export. Adapt and +test it against a redacted export before use; do not deploy it as-is. +::: +:::warning Auth0 does not expose password hashes or `TOTP` device information. You will have to contact their support separately if you need this type of data. ::: +Create the Auth0 roles in SuperTokens before migrating users. The application endpoint must own an allowlisted mapping +from Auth0 organization/connection/provider identifiers to SuperTokens tenants and providers. Do not let Action input +select arbitrary tenant IDs or provider configuration. + ##### 1. Access the Auth0 Dashboard ##### 2. From the navigation menu go to *Actions* > *Library* ##### 3. Click *Create Action* > *Create custom action* ##### 4. Specify a custom name for your action and then select *Login/Post Login* as the trigger -##### 5. After the action has been created, click the *Add Secret* button and save your `SUPERTOKENS_CORE_API_KEY` as a secret -##### 5. Paste the following code in the editor +##### 5. Add `MIGRATION_ENDPOINT_URL` and `MIGRATION_ENDPOINT_TOKEN` Action secrets +##### 6. Paste the following code in the editor -```typescript +`MIGRATION_ENDPOINT_TOKEN` must authorize only this migration endpoint. The endpoint must authenticate every request, +allow only the expected Auth0 tenant/issuer, rate-limit by credential and legacy user ID, enforce request-size limits, +and use `externalUserId` as an idempotency key. Keep the Core URL and Core API key only in your backend secret store. The +backend validates and maps the identity, retrieves any credential export through restricted storage, and then calls Core. + +```typescript check=false reason="Requires application specific migration types" exports.onExecutePostLogin = async (event, api) => { - const SUPERTOKENS_CORE_URL = ""; - const SUPERTOKENS_API_KEY = event.secrets.SUPERTOKENS_API_KEY; - const auth0User = event.user; - + const migrationEndpoint = event.secrets.MIGRATION_ENDPOINT_URL; + const migrationToken = event.secrets.MIGRATION_ENDPOINT_TOKEN; + try { - if (auth0User.app_metadata?.migrated_to_supertokens) { - console.log(`User ${auth0User.user_id} already migrated`); + if (event.user.app_metadata?.migrated_to_supertokens) { return; } - - const userPayload = { - externalUserId: auth0User.user_id, - userMetadata: { - auth0_user_id: auth0User.user_id, - name: auth0User.name, - nickname: auth0User.nickname, - picture: auth0User.picture - auth0_user_metadata: auth0User.user_metadata, - auth0_app_metadata: auth0User.app_metadata - }, - roles: auth0User.app_metadata?.roles || [], - loginMethods: [] - }; - const ThirdPartyProviders = ['google-oauth2', 'facebook', 'github', 'apple']; - - auth0User.identities.forEach((identity, index) => { - if(ThirdPartyProviders.includes(identity.provider)) { - userPayload.loginMethods.push({ - recipeId: "thirdparty", - thirdPartyId: mapProvider(identity.provider), - thirdPartyUserId: identity.user_id, - email: identity.profileData?.email ?? auth0User.email, - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } - } else if (identity.provider === 'auth0' || identity.provider === 'Username-Password-Authentication') { - // Auth0 does not export passworded hashes by default - // You will have to contact their support and request them - userPayload.loginMethods.push({ - recipeId: "emailpassword", - email: identity.profileData?.email ?? auth0User.email, - // Request the password hash from Auth0 and then implement the function to retrieve the values - passwordHash: getPasswordHash(identity.profileData?.email), - hashingAlgorithm: "bcrypt", - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else if (identity.provider === 'sms') { - userPayload.loginMethods.push({ - recipeId: "passwordless", - phoneNumber: identity.profileData?.phone_number ?? auth0User.phone_number, - isVerified: identity.profileData?.phone_verified ?? auth0User.phone_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else if (identity.provider === 'email') { - userPayload.loginMethods.push({ - recipeId: "passwordless", - email: identity.profileData?.email || auth0User.email, - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else { - throw new Error(`Uknown provider: ${identity.provider}`); - } - }); - - const response = await fetch(`${SUPERTOKENS_CORE_URL}/bulk-import/import`, { - method: 'POST', + const response = await fetch(migrationEndpoint, { + method: "POST", headers: { - 'api-key': SUPERTOKENS_API_KEY, - 'Content-Type': 'application/json; charset=utf-8' + Authorization: `Bearer ${migrationToken}`, + "Content-Type": "application/json; charset=utf-8", }, - body: JSON.stringify(userPayload) + body: JSON.stringify({ + externalUserId: event.user.user_id, + auth0OrganizationId: event.organization?.id, + identities: event.user.identities?.map(({ provider, connection, user_id }) => ({ + provider, + connection, + userId: user_id, + })), + }), }); - + const result = await response.json(); - - if (response.ok && result.status === 'OK') { - console.log(`User migrated: ${event.user.email || event.user.user_id}`); - api.user.setAppMetadata('migrated_to_supertokens', true); - api.user.setAppMetadata('supertokens_user_id', result.user?.id); + + if (response.ok && result.status === "OK") { + api.user.setAppMetadata("migrated_to_supertokens", true); + api.user.setAppMetadata("supertokens_user_id", result.userId); } else { - console.error(`Migration failed: ${JSON.stringify(result)}`); + console.error("Migration endpoint rejected the request"); } - } catch (error) { - console.error('Migration error:', error.message); + console.error("Migration endpoint request failed"); } }; - -function mapProvider(strategy) { - const mapping = { - 'google-oauth2': 'google', - 'facebook': 'facebook', - 'github': 'github', - 'apple': 'apple', - }; - return mapping[strategy] || strategy; -} ``` - + -
+
-:::info -If your application does not have a sign up process or if new users get created manually you can skip this step +:::info[If your application does not have a sign up process or if new users get created manually you can skip this step] ::: - - - -```bash -curl --location --request POST '^{coreInfo.uri}/bulk-import/import' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data ' - { - "externalUserId": "user_12345", - "userMetadata": { - "firstName": "Jane", - "lastName": "Doe", - "department": "Engineering" - }, - "userRoles": [{ "role": "admin", "tenantIds": [] }], - "totpDevices": [ - { - "secretKey": "JBSWY3DPEHPK3PXP", - "period": 30, - "skew": 1, - "deviceName": "Main Device" - } - ], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672531199000, - "recipeId": "emailpassword", - "email": "jane.doe@example.com", - "passwordHash": "$2b$12$KIXQeFz...", - "hashingAlgorithm": "bcrypt" - } - ] - } -' -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/bulk-import/import`; - const options = { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - externalUserId: "user_12345", - userRoles: [{ role: "admin", tenantIds: [] }], - userMetadata: { - firstName: "Jane", - lastName: "Doe", - department: "Engineering" - }, - totpDevices: [ - { - secretKey: "JBSWY3DPEHPK3PXP", - period: 30, - skew: 1, - deviceName: "Main Device" - } - ], - loginMethods: [ - { - isVerified: true, - isPrimary: true, - timeJoinedInMSSinceEpoch: 1672531199000, - recipeId: "emailpassword", - email: "jane.doe@example.com", - passwordHash: "$2b$12$KIXQeFz...", - hashingAlgorithm: "bcrypt" - } - ] - }) - } - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); - ``` - - - - - ```go - - import ( - "fmt" - "net/http" - "strings" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/bulk-import/import", baseUrl) - payload := `{ - "externalUserId": "user_12345", - "userMetadata": { - "firstName": "Jane", - "lastName": "Doe", - "department": "Engineering" - }, - "userRoles": [{ "role": "admin", "tenantIds": [] }], - "totpDevices": [ - { - "secretKey": "JBSWY3DPEHPK3PXP", - "period": 30, - "skew": 1, - "deviceName": "Main Device" - } - ], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672531199000, - "recipeId": "emailpassword", - "email": "jane.doe@example.com", - "passwordHash": "$2b$12$KIXQeFz...", - "hashingAlgorithm": "bcrypt" - } - ] - }` - - req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - from typing import Dict, Any - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/bulk-import/import" - - payload: Dict[str, Any] = { - "externalUserId": "user_12345", - "userMetadata": { - "firstName": "Jane", - "lastName": "Doe", - "department": "Engineering" - }, - "userRoles": [{ "role": "admin", "tenantIds": [] }], - "totpDevices": [ - { - "secretKey": "JBSWY3DPEHPK3PXP", - "period": 30, - "skew": 1, - "deviceName": "Main Device" - } - ], - "loginMethods": [ - { - "isVerified": True, - "isPrimary": True, - "timeJoinedInMSSinceEpoch": 1672531199000, - "recipeId": "emailpassword", - "email": "jane.doe@example.com", - "passwordHash": "$2b$12$KIXQeFz...", - "hashingAlgorithm": "bcrypt" - } - ] - } - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - ``` - - - - - - - - - ### 2. Export the accounts from your legacy provider -Export the users from your legacy authentication provider and adjust the data to match the request body schema used in the [**`Add Users for Bulk Import`**](#add-users-for-bulk-import-http-request) endpoint. +Export the users from your legacy authentication provider and adjust the data to match the request body schema used in the [**`Add Users for Bulk Import`**](/references/cdi/bulk-import/addbulkimportusers) endpoint. - -## Auth0 Instructions -:::warning no-title + + +:::warning Auth0 does not export password hashes or `TOTP` device information. You will have to contact their support and request them. ::: @@ -414,7 +153,7 @@ You will have to contact their support and request them. ##### 1.2 Select `Auth0 Management API` ##### 1.3 Go to `Machine to Machine Applications` tab ##### 1.4 Authorize your application or create a new one -##### 1.5 Grant the following permissions: `read:users`, `update:users`, `create:users`, `read:user_app_metadata` +##### 1.5 Grant only `read:users` and `read:users_app_metadata` ##### 1.6 Save your `Domain`, `Client ID`, and `Client Secret` #### 2. Get the management API access token @@ -433,7 +172,7 @@ curl --request POST \ ``` #### 3. Create the export job -Use the `POST /api/v2/jobs/users-exports` endpoint to create a job that exports all users. +Use the `POST /api/v2/jobs/users-exports` endpoint to create a job that exports all users. ```bash curl --request POST \ @@ -476,115 +215,136 @@ The previous request returns a `location` attribute in the response body if the Use it do access your data. ```bash -curl -o auth0_users.json.gz "LOCATION_URL_FROM_RESPONSE" -gunzip auth0_users.json.gz +umask 077 +cd /secure-migration-work # An encrypted, access-restricted filesystem +curl --fail --location --proto '=https' -o auth0_users.json.gz "LOCATION_URL_FROM_RESPONSE" +gzip --test auth0_users.json.gz +sha256sum auth0_users.json.gz > auth0_users.json.gz.sha256 +age --recipient "" --output auth0_users.json.gz.age auth0_users.json.gz ``` -Auth0 exports data in `NDJSON` format. -Convert it to JSON first: +Keep the compressed download on that encrypted filesystem, move the encrypted archive and checksum to restricted +migration storage, and verify that decryption succeeds. Auth0 exports NDJSON inside the gzip stream. Convert it without +replacing the retained compressed archive: ```bash -cat auth0_users.json | jq -s '.' > auth0_users_array.json +age --decrypt --identity /run/secrets/migration-archive-key auth0_users.json.gz.age \ + | gzip -dc \ + | jq -s '.' > /secure-migration-work/auth0_users_array.json ``` +Keep the compressed export, encrypted copy, and checksum unchanged through transformation, import, failed-row retries, +and source-to-target reconciliation. Keep derived plaintext only on encrypted restricted storage and delete it after +each run. Delete all source-archive copies only after final reconciliation and rollback retention requirements are met; +use your storage system's verified deletion/lifecycle mechanism rather than assuming `rm` securely erases every medium. + #### 6. Transform the data to the SuperTokens format -:::warning no-title +:::warning Auth0 does not expose password hashes or `TOTP` device information. You will have to contact their support separately if you need this type of data. ::: -```typescript -const fs = require('fs'); +Create the Auth0 roles in SuperTokens before migrating users. This example assigns them to the default `public` tenant. -const auth0Users = JSON.parse(fs.readFileSync('auth0_users_array.json', 'utf8')); +:::caution[Unverified mapping pseudocode] +The transformation below assumes application-specific Auth0 identity fields and an undefined `getPasswordHash` lookup. +The relationship between ordinary user-export rows and a separately requested password/MFA export is not established +here. Validate the mapping against a redacted current export and Core import validation before handling production data. +::: -const superTokensUsers = auth0Users.map(auth0User => { - if(auth0User.user_metadata?.migrated_to_supertokens) { - console.log(`User ${auth0User.user_id} already migrated`); - return; - } +```typescript check=false reason="Requires application specific migration types" +const fs = require("fs"); - const userPayload = { - externalUserId: event.user.user_id, - userMetadata: { - auth0_user_id: auth0User.user_id, - name: auth0User.name, - nickname: auth0User.nickname, - picture: auth0User.picture, - auth0_user_metadata: auth0User.user_metadata - auth0_app_metadata: auth0User.app_metadata - }, - roles: auth0User.app_metadata?.roles || [], - loginMethods: [], - }; +const auth0Users = JSON.parse(fs.readFileSync("auth0_users_array.json", "utf8")); - const ThirdPartyProviders = ['google-oauth2', 'facebook', 'github', 'apple']; - - auth0User.identities.forEach((identity, index) => { - if(ThirdPartyProviders.includes(identity.provider)) { - userPayload.loginMethods.push({ - recipeId: "thirdparty", - thirdPartyId: mapProvider(identity.provider), - thirdPartyUserId: identity.user_id, - email: identity.profileData?.email ?? auth0User.email, - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } - } else if (identity.provider === 'auth0' || identity.provider === 'Username-Password-Authentication') { - // Auth0 does not export passworded hashes by default - // You will have to contact their support and request them - userPayload.loginMethods.push({ - recipeId: "emailpassword", - email: identity.profileData?.email ?? auth0User.email, - // Request the password hash from Auth0 and then implement the function to retrieve the values - passwordHash: getPasswordHash(identity.profileData?.email), - hashingAlgorithm: "bcrypt", - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else if (identity.provider === 'sms') { - userPayload.loginMethods.push({ - recipeId: "passwordless", - phoneNumber: identity.profileData?.phone_number || auth0User.phone_number, - isVerified: identity.profileData?.phone_verified ?? auth0User.phone_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else if (identity.provider === 'email') { - userPayload.loginMethods.push({ - recipeId: "passwordless", - email: identity.profileData?.email || auth0User.email, - isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, - isPrimary: index === 0, - timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime() - }); - } else { - throw new Error(`Uknown provider: ${identity.provider}`); +const superTokensUsers = auth0Users + .map((auth0User) => { + if (auth0User.app_metadata?.migrated_to_supertokens) { + console.log(`User ${auth0User.user_id} already migrated`); + return; } - }); - - return userPayload; -}); -fs.writeFileSync('supertokens_users.json', JSON.stringify({ users: superTokensUsers }, null, 2)); + const userPayload = { + externalUserId: auth0User.user_id, + userMetadata: { + auth0_user_id: auth0User.user_id, + name: auth0User.name, + nickname: auth0User.nickname, + picture: auth0User.picture, + auth0_user_metadata: auth0User.user_metadata, + auth0_app_metadata: auth0User.app_metadata, + }, + userRoles: (auth0User.app_metadata?.roles || []).map((role) => ({ role, tenantIds: ["public"] })), + loginMethods: [], + }; + + const ThirdPartyProviders = ["google-oauth2", "facebook", "github", "apple"]; + + auth0User.identities.forEach((identity, index) => { + if (ThirdPartyProviders.includes(identity.provider)) { + userPayload.loginMethods.push({ + recipeId: "thirdparty", + thirdPartyId: mapProvider(identity.provider), + thirdPartyUserId: identity.user_id, + email: identity.profileData?.email ?? auth0User.email, + isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, + isPrimary: index === 0, + timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), + }); + } else if (identity.provider === "auth0" || identity.provider === "Username-Password-Authentication") { + // Auth0 does not export password hashes through the ordinary user export + // You will have to contact their support and request them + userPayload.loginMethods.push({ + recipeId: "emailpassword", + email: identity.profileData?.email ?? auth0User.email, + // Request the password hash from Auth0 and then implement the function to retrieve the values + passwordHash: getPasswordHash(identity.profileData?.email), + hashingAlgorithm: "bcrypt", + isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, + isPrimary: index === 0, + timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), + }); + } else if (identity.provider === "sms") { + userPayload.loginMethods.push({ + recipeId: "passwordless", + phoneNumber: identity.profileData?.phone_number || auth0User.phone_number, + isVerified: identity.profileData?.phone_verified ?? auth0User.phone_verified ?? false, + isPrimary: index === 0, + timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), + }); + } else if (identity.provider === "email") { + userPayload.loginMethods.push({ + recipeId: "passwordless", + email: identity.profileData?.email || auth0User.email, + isVerified: identity.profileData?.email_verified ?? auth0User.email_verified ?? false, + isPrimary: index === 0, + timeJoinedInMSSinceEpoch: new Date(auth0User.created_at).getTime(), + }); + } else { + throw new Error(`Unknown provider: ${identity.provider}`); + } + }); + + return userPayload; + }) + .filter(Boolean); + +fs.writeFileSync("supertokens_users.json", JSON.stringify({ users: superTokensUsers }, null, 2)); function mapProvider(auth0Provider) { const mapping = { - 'google-oauth2': 'google', - 'facebook': 'facebook', - 'github': 'github', - 'apple': 'apple' + "google-oauth2": "google", + facebook: "facebook", + github: "github", + apple: "apple", }; return mapping[auth0Provider] || auth0Provider; } console.log(`Transformed ${superTokensUsers.length} users`); ``` - + ### 3. Perform the bulk migration process @@ -592,7 +352,7 @@ console.log(`Transformed ${superTokensUsers.length} users`); :::warning If your application has a sign up process please make sure that you have completed the [**first step**](#1-update-the-legacy-sign-up-flow). -Otherwise, new accounts that get created after you have exported your users are not available in **SuperTokens**. +Otherwise, new accounts that get created after you have exported your users are not available in **SuperTokens**. ::: @@ -604,194 +364,9 @@ This step stages the data that the background job imports later. Keep in mind that the endpoint has a limit of **10000 users** per request. + - - - -```bash -curl --location --request POST '^{coreInfo.uri}/bulk-import/users' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - users: [ - { - "externalUserId": "user_2", - "userMetadata": { - "firstName": "John", - "lastName": "Doe", - "department": "Marketing" - }, - "userRoles": [{ "role": "editor", "tenantIds": [] }], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672617599000, - "recipeId": "thirdparty", - "email": "john.doe@gmail.com", - "thirdPartyId": "google", - "thirdPartyUserId": "google_987654321" - } - ] - } - ] -} -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/bulk-import/users`; - const options = { - method: 'POST', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify({ - users: [ - { - externalUserId: "user_2", - userMetadata: { - firstName: "John", - lastName: "Doe", - department: "Marketing" - }, - userRoles: [{ role: "editor", tenantIds: [] }], - loginMethods: [ - { - isVerified: true, - isPrimary: true, - timeJoinedInMSSinceEpoch: 1672617599000, - recipeId: "thirdparty", - email: "john.doe@gmail.com", - thirdPartyId: "google", - thirdPartyUserId: "google_987654321" - } - ] - } - ] - }) - } - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); - ``` - - - - - ```go - import ( - "fmt" - "net/http" - "strings" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/bulk-import/users", baseUrl) - payload := `{ - "users": [ - { - "externalUserId": "user_2", - "userMetadata": { - "firstName": "John", - "lastName": "Doe", - "department": "Marketing" - }, - "userRoles": [{ "role": "editor", "tenantIds": [] }], - "loginMethods": [ - { - "isVerified": true, - "isPrimary": true, - "timeJoinedInMSSinceEpoch": 1672617599000, - "recipeId": "thirdparty", - "email": "john.doe@gmail.com", - "thirdPartyId": "google", - "thirdPartyUserId": "google_987654321" - } - ] - } - ] - }` - - req, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - from typing import Dict, Any - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/bulk-import/users" - - payload: Dict[str, Any] = { - "users": [ - { - "externalUserId": "user_2", - "userMetadata": { - "firstName": "John", - "lastName": "Doe", - "department": "Marketing" - }, - "userRoles": [{ "role": "editor", "tenantIds": [] }], - "loginMethods": [ - { - "isVerified": True, - "isPrimary": True, - "timeJoinedInMSSinceEpoch": 1672617599000, - "recipeId": "thirdparty", - "email": "john.doe@gmail.com", - "thirdPartyId": "google", - "thirdPartyUserId": "google_987654321" - } - ] - } - ] - } - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - ``` - - - - - - - -:::info The Bulk Import Cron Job +:::info[The Bulk Import Cron Job] Every 5 minutes the **SuperTokens** core service runs a cron job that goes through the staged users and tries to import them. If a user gets imported successfully it gets removed from the staged list. @@ -801,7 +376,7 @@ If a user gets imported successfully it gets removed from the staged list. #### 3.2 Monitor the progress of the job -To determine if the import flow has processed all the users call the [`Count Staged Users`](#count-staged-users-http-request) API. +To determine if the import flow has processed all the users, call the [`Count Staged Users`](/references/cdi/bulk-import/countbulkimportusers) API. Before doing that, first understand the different states in which a staged user can be. During the import process, the user can have one of the following statuses: @@ -813,7 +388,13 @@ If a user gets imported successfully it then gets removed from the staged list. With this new information, get back to the `count users` endpoint. The request counts the users that await import. -Pass a status filter as a query parameter (for example, `status=NEW`) to count only the users with that status. +Pass a status filter as a query parameter to count only users in that state: `status=NEW`, `status=PROCESSING`, or `status=FAILED`. + + Given that information, to check if your import is complete do the following: 1. Call the `count users` API once without any filters. If the count is 0, then the import process is complete. @@ -821,96 +402,6 @@ Given that information, to check if your import is complete do the following: 3. If the only rows that remain are the ones with the `FAILED` status, then proceed to step `3.3`. There you can see how to debug those issues. - - - -```bash - curl --location --request GET '^{coreInfo.uri}/bulk-import/users/count?status=PROCESSING' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ -``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/bulk-import/users/count?status=PROCESSING`; - const options = { - method: 'GET', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - } - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); - ``` - - - - - ```go - import ( - "fmt" - "net/http" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/bulk-import/users/count?status=PROCESSING", baseUrl) - - req, _ := http.NewRequest("GET", url, nil) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/bulk-import/users/count?status=PROCESSING" - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, headers=headers) - - print(response.json()) - ``` - - - - - - - - - #### 3.3 Handle staged users that failed to import Go through this step only if you have staged users that failed to import. @@ -919,119 +410,40 @@ This can happen for a number of reasons. Some common ones: - `externalUserId` is being already used by other user - A primary user already exists for the email but with a different login method -If at the end of the previous step you have determined that you have staged users that failed to import debug the issues with the `get users` API. - - - - - ```bash - curl --location --request GET '^{coreInfo.uri}/bulk-import/users?status=FAILED' \ - --header 'api-key: ^{coreInfo.key}' \ - --header 'Content-Type: application/json; charset=utf-8' \ - ``` - - - - - ```tsx - const BASE_URL = '^{coreInfo.uri}'; - const API_KEY = '^{coreInfo.key}'; - - const url = `${BASE_URL}/bulk-import/users?status=FAILED`; - const options = { - method: 'GET', - headers: { - 'api-key': API_KEY, - 'Content-Type': 'application/json; charset=utf-8', - }, - } - - fetch(url, options) - .then(response => response.json()) - .then(json => console.log(json)) - .catch(err => console.error(err)); - ``` - - - - - ```go - import ( - "fmt" - "net/http" - "io" - ) - - func main() { - baseUrl := "^{coreInfo.uri}" - apiKey := "^{coreInfo.key}" - url := fmt.Sprintf("%s/bulk-import/users?status=FAILED", baseUrl) - - req, _ := http.NewRequest("GET", url, nil) - - req.Header.Add("accept", "application/json") - req.Header.Add("api-key", apiKey) - req.Header.Add("content-type", "application/json") - - res, _ := http.DefaultClient.Do(req) - - defer res.Body.Close() - body, _ := io.ReadAll(res.Body) - - fmt.Println(string(body)) - } - ``` - - - - - ```python - import requests - - BASE_URL = "^{coreInfo.uri}" - API_KEY = "^{coreInfo.key}" - - url = f"{BASE_URL}/bulk-import/users?status=FAILED" - - headers = { - "api-key": API_KEY, - "Content-Type": "application/json", - } - - response = requests.post(url, headers=headers) - - print(response.json()) - ``` - - - - - - - +If at the end of the previous step you have determined that you have staged users that failed to import, debug the issues with the [`Get Staged Users`](/references/cdi/bulk-import/getbulkimportusers) API. Filter the results with `status=FAILED`. + The response includes the import error messages for each specific user. Use them to determine what you need to correct in your import data. -After you have fixed the issues, run the process again, from step `3.1`, only for the corrected data. +Record the failed staged-row IDs and remove those exact rows before retrying. Verify that every requested ID appears in +`deletedIds` and that `invalidIds` is empty; otherwise, stop and reconcile the discrepancy. + +After removal, fix the source records and repeat step `3.1` only for that corrected data. Re-run the status checks and +reconcile every source identity to one successfully imported account. Never treat a zero count as sufficient if the +source export, removed IDs, corrected retries, and final accounts do not reconcile. -:::success You have successfully migrated your accounts + +:::success[You have successfully migrated your accounts] If all your data has imported then you can consider the account migration process complete. -Go on to the [session migration](/docs/migration/session-migration) step to complete the entire migration flow. +Go on to the [session migration](/migration/session-migration) step to complete the entire migration flow. ::: ## See also - - - - - - - + + + + + + diff --git a/docs/migration/legacy/_category_.json b/docs/migration/legacy/_category_.json deleted file mode 100644 index 588fbdf24f..0000000000 --- a/docs/migration/legacy/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Legacy Method", - "collapsible": true, - "collapsed": true, - "customProps": {}, - "position": 4 -} diff --git a/docs/migration/legacy/about.mdx b/docs/migration/legacy/about.mdx index c72f329045..fdbdab862a 100644 --- a/docs/migration/legacy/about.mdx +++ b/docs/migration/legacy/about.mdx @@ -1,14 +1,9 @@ --- -id: about title: About -hide_title: true -sidebar_position: 1 -page_type: overview -category: migration +sidebar: + order: 1 --- -# About - In this guide we will be going through the process of migrating users from an external Authentication provider to SuperTokens. User migration involves 3 steps: @@ -47,16 +42,16 @@ User migration involves 3 steps: You can learn more about how to implement these changes in the [Session Migration](./session-migration) section. ## Step 4. MFA migration -If you are using MFA in your app, checkout the MFA migration section [here](/docs/additional-verification/mfa/migration/legacy-to-new) after you have gone through the above migration steps. +If you are using MFA in your app, checkout the MFA migration section [here](/additional-verification/mfa/migration/legacy-to-new) after you have gone through the above migration steps. --- ## See also - - - - - - - + + + + + + + diff --git a/docs/migration/legacy/account-creation/_category_.json b/docs/migration/legacy/account-creation/_category_.json deleted file mode 100644 index 7f6ff1cac7..0000000000 --- a/docs/migration/legacy/account-creation/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Step 1: Account Creation", - "position": 2 -} diff --git a/docs/migration/legacy/account-creation/email-verification.mdx b/docs/migration/legacy/account-creation/email-verification.mdx index 399d794e5a..baded390a5 100644 --- a/docs/migration/legacy/account-creation/email-verification.mdx +++ b/docs/migration/legacy/account-creation/email-verification.mdx @@ -1,14 +1,9 @@ --- -id: email-verification title: Mark email as verified -hide_title: true -sidebar_position: 3 -page_type: guide -category: migration +sidebar: + order: 3 --- -# Mark email as verified - Once a SuperTokens user has been created and their userId has been mapped, you need to mark their email as verified, if their email was verified in the old auth provider. ## Step 1. Generating the email verification token: @@ -16,8 +11,8 @@ Once a SuperTokens user has been created and their userId has been mapped, you n For example with the email as `johnDoe@gmail.com` and userId as `056f4b02-c992-42ed-a8af-cb709669bbd` ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/user/email/verify/token' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/user/email/verify/token' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johnDoe@gmail.com", @@ -39,8 +34,8 @@ Successfully generating an email verification token will result in the following Retrieve the token from the response of the previous request and set it in the body of the email verification request. ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/user/email/verify' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/user/email/verify' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "method": "token", diff --git a/docs/migration/legacy/account-creation/ep-migration-without-password-hash.mdx b/docs/migration/legacy/account-creation/ep-migration-without-password-hash.mdx index 0bb01568aa..dc4d5a6d0d 100644 --- a/docs/migration/legacy/account-creation/ep-migration-without-password-hash.mdx +++ b/docs/migration/legacy/account-creation/ep-migration-without-password-hash.mdx @@ -1,20 +1,28 @@ --- -id: ep-migration-without-password-hash title: User Creation without password hashes -hide_title: true -sidebar_position: 4 -page_type: guide -category: migration +sidebar: + order: 4 --- - -# User Creation without password hashes - -:::caution +:::warning The recommended method for migrating users to SuperTokens is by [importing users with their password hashes](./user-creation). You should only use the following method if you do not have access to your user's password hashes and still have access to your previous identity provider. ::: +:::danger[Add concurrency and recovery controls before production use] +The handlers below are partial examples; the external-provider functions and application locking are intentionally not +implemented. Sign-up, user-ID mapping, email verification, and metadata updates are separate writes, not one transaction. +For each normalized `(tenantId, email)` identity, serialize migration in a distributed lock, re-read the SuperTokens user +and user-ID mapping after acquiring it, and make retries resume only missing steps. Treat an existing mapping as +idempotent only when both its SuperTokens and external IDs match the intended user. Abort on every conflicting mapping, +tenant mismatch, or response other than success, and reconcile partially created users before retrying. + +Initialize every recipe called by a handler in the same SDK initialization: Email Password, Email Verification in the +mode used by your application, and User Metadata where metadata is read or written. Preserve the `RecipeUserId` returned +by sign-up for recipe operations; user-ID mapping does not replace it with the external ID. Test concurrent requests, +crashes after each write, retries, and mapping conflicts before enabling these overrides. +::: + SuperTokens also supports the "**in time**" user migration strategy for when password hashes cannot be exported from your legacy provider. We need to make the following customizations to SuperTokens authentication flows to support this strategy: @@ -36,48 +44,56 @@ We need to make the following customizations to SuperTokens authentication flows To implement this change we override the API that handles email-password login when initializing the recipe on the backend. - - - + + ```tsx -import EmailPassword from "supertokens-node/recipe/emailpassword" - -EmailPassword.init({ - override: { - apis: (originalImplementation) => { +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import Session from "supertokens-node/recipe/session"; +const emailPasswordRecipe = EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + let email = input.formFields.find((field) => field.id === "email")!.value as string; + // Check if the user signing in exists in the external provider + if (await doesUserExistInExternalProvider(email)) { + // Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider return { - ...originalImplementation, - signUpPOST: async function (input) { - let email = input.formFields.find((field) => field.id === "email")!.value as string; - // Check if the user signing in exists in the external provider - if (await doesUserExistInExternalProvider(email)) { - // Return status "EMAIL_ALREADY_EXISTS_ERROR" since the user already exists in the external provider - return { - status: "EMAIL_ALREADY_EXISTS_ERROR" - } - } - return originalImplementation.signUpPOST!(input); - }, - } + status: "EMAIL_ALREADY_EXISTS_ERROR", + }; + } + return originalImplementation.signUpPOST!(input); }, - } -}) + }; + }, + }, +}); + +SuperTokens.init({ + framework: "express", + supertokens: { connectionURI: "" }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [emailPasswordRecipe, Session.init()], +}); async function doesUserExistInExternalProvider(email: string): Promise { - // TODO: check if user with the input email exists in the external provider - return false; + // TODO: check if user with the input email exists in the external provider + return false; } ``` - - - - - -```python + + +```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init -from supertokens_python.recipe import emailpassword +from supertokens_python.recipe import emailpassword, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, @@ -127,27 +143,26 @@ async def does_user_exist_in_external_provider(email: str): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig( apis=override_email_password_apis, ) - ) + ), + session.init(), ], ) ``` - - - - - + + ```go import ( "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" + "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) @@ -186,6 +201,7 @@ func main() { }, }, }), + session.Init(nil), }, }) } @@ -195,10 +211,8 @@ func doesUserExistInExternalProvider(email string) bool { return false } ``` - - - - + + We modify the `signUpPOST` API to first check if the user signing up has an account with the external provider. If they do we return a `EMAIL_ALREADY_EXISTS_ERROR` @@ -206,89 +220,126 @@ We modify the `signUpPOST` API to first check if the user signing up has an acco To implement this flow we override the API that handles email-password login when initializing the recipe on the backend. - - - + + ```tsx -import SuperTokens from "supertokens-node" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import EmailVerification from "supertokens-node/recipe/emailverification" -import { RecipeUserId } from "supertokens-node"; - -EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInPOST: async function (input) { - // Check if an email-password user with the input email exists in SuperTokens - let email = input.formFields.find((field) => field.id === "email")!.value as string; - let password = input.formFields.find((field) => field.id === "password")!.value as string; - let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: email - }, undefined, input.userContext); - let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; - }) - - if (emailPasswordUser === undefined) { - // EmailPassword user with the input email does not exist in SuperTokens - // Check if the input credentials are valid in the external provider - let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) - if (legacyUserInfo === undefined) { - // credentials are incorrect - return { - status: "WRONG_CREDENTIALS_ERROR" - } - } - - // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); - if (signUpResponse.status !== "OK") { - throw new Error("Should never come here") - } - - // Map the external provider's userId to the SuperTokens userId - await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id }) - - // Set the userId in the response to use the provider's userId - signUpResponse.user.id = legacyUserInfo.user_id - signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id); - signUpResponse.recipeUserId = new RecipeUserId(legacyUserInfo.user_id); - - // We also need to set the email verification status of the user - if (legacyUserInfo.isEmailVerified) { - // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext); - - if (generateEmailVerificationTokenResponse.status === "OK") { - // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); - } - } - } - - return originalImplementation.signInPOST!(input) - }, +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import EmailVerification from "supertokens-node/recipe/emailverification"; +import Session from "supertokens-node/recipe/session"; +const emailPasswordRecipe = EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInPOST: async function (input) { + // Check if an email-password user with the input email exists in SuperTokens + let email = input.formFields.find((field) => field.id === "email")!.value as string; + let password = input.formFields.find((field) => field.id === "password")!.value as string; + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( + input.tenantId, + { + email: email, + }, + undefined, + input.userContext, + ); + let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { + return ( + u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined + ); + }); + + if (emailPasswordUser === undefined) { + // EmailPassword user with the input email does not exist in SuperTokens + // Check if the input credentials are valid in the external provider + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); + if (legacyUserInfo === undefined) { + // credentials are incorrect + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + + // Call the signup function to create a new SuperTokens user. + let signUpResponse = await EmailPassword.signUp( + input.tenantId, + email, + password, + undefined, + input.userContext, + ); + if (signUpResponse.status !== "OK") { + throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); + } + + // Map the external provider's userId to the SuperTokens userId + const mappingResponse = await SuperTokens.createUserIdMapping({ + superTokensUserId: signUpResponse.user.id, + externalUserId: legacyUserInfo.user_id, + userContext: input.userContext, + }); + if (mappingResponse.status !== "OK") { + throw new Error("Legacy user ID conflicts with an existing mapping"); + } + + // We also need to set the email verification status of the user + if (legacyUserInfo.isEmailVerified) { + // Generate an email verification token for the user + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( + input.tenantId, + signUpResponse.recipeUserId, + email, + input.userContext, + ); + + if (generateEmailVerificationTokenResponse.status === "OK") { + // Verify the user's email + await EmailVerification.verifyEmailUsingToken( + input.tenantId, + generateEmailVerificationTokenResponse.token, + undefined, + input.userContext, + ); + } } + } + + return originalImplementation.signInPOST!(input); }, + }; + }, + }, +}); + +SuperTokens.init({ + framework: "express", + supertokens: { connectionURI: "" }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" })], +}); + +async function validateAndGetUserInfoFromExternalProvider( + email: string, + password: string, +): Promise< + | { + user_id: string; + isEmailVerified: boolean; } -}) - -async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{ - user_id: string, - isEmailVerified: boolean -} | undefined> { - // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. - return undefined + | undefined +> { + // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. + return undefined; } ``` - - - - - -```python + + +```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init @@ -296,7 +347,7 @@ from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) -from supertokens_python.recipe import emailpassword +from supertokens_python.recipe import emailpassword, emailverification, session from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, @@ -313,7 +364,7 @@ from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.session.interfaces import SessionContainer -from supertokens_python.types import RecipeUserId +from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput @@ -364,17 +415,14 @@ def override_emailpassword_apis(original_implementation: APIInterface): # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): - raise Exception("Should never come here") + raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the external provider's userId to the SuperTokens userId - await create_user_id_mapping(response.user.id, legacy_user_info.user_id) - # Set the userId in the response to use the provider's userId - response.user.id = legacy_user_info.user_id - response.user.login_methods[0].recipe_user_id = RecipeUserId( - legacy_user_info.user_id + mapping_response = await create_user_id_mapping( + response.user.id, legacy_user_info.user_id, user_context=user_context ) - response.recipe_user_id = RecipeUserId(legacy_user_info.user_id) - + if not isinstance(mapping_response, CreateUserIdMappingOkResult): + raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user @@ -424,19 +472,18 @@ async def validate_and_get_user_info_from_external_provider( init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) - ) + ), + emailverification.init("OPTIONAL"), + session.init(), ], ) ``` - - - - - + + ```go import ( "errors" @@ -444,6 +491,8 @@ import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" + "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" + "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) @@ -498,22 +547,23 @@ func main() { return epmodels.SignInPOSTResponse{}, err } - if response.OK == nil { - return epmodels.SignInPOSTResponse{}, errors.New("Should never come here") - } + if response.OK == nil { + return epmodels.SignInPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") + } + recipeUserId := response.OK.User.ID - // Map the external provider's userId to the SuperTokens userId - _, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil) + // Map the external provider's userId to the SuperTokens userId + mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.SignInPOSTResponse{}, err } - // Set the userId in the response to use the provider's userId - response.OK.User.ID = legacyUserInfo.userId - + if mapping.OK == nil { + return epmodels.SignInPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") + } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { // Generate an email verification token for the user - generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) + generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &email) if err != nil { return epmodels.SignInPOSTResponse{}, err } @@ -532,6 +582,8 @@ func main() { }, }, }), + emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), + session.Init(nil), }, }) } @@ -546,10 +598,8 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * return nil } ``` - - - - + + The code above overrides the `signInPOST` API with the following changes to achieve "**in time**" migration: - The first step is to determine if the user signing in needs to be migrated or not. We do this by checking if a user with the input email exists in the external auth provider and SuperTokens. @@ -563,99 +613,133 @@ If the credentials are valid we can call the SuperTokens login function with the ## Step 3) Create a SuperTokens account for users who have an account with the external provider but have forgotten their password. Some users who do not have an account with SuperTokens but have an existing account with the external provider may have forgotten their passwords and initiate a password reset. -Since password resets require an existing SuperTokens account to send the password reset email to, the password reset flow needs to be modified to create a SuperTokens account if the user exists in the external provider. - - - +Since password resets require an existing SuperTokens account to send the password reset email to, the password reset flow needs to be modified to create a SuperTokens account if the user exists in the external provider. + + ```tsx -import SuperTokens from "supertokens-node" -import EmailPassword from "supertokens-node/recipe/emailpassword" -import EmailVerification from "supertokens-node/recipe/emailverification" -import UserMetadata from "supertokens-node/recipe/usermetadata" -import { RecipeUserId } from "supertokens-node"; - -EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - // Add overrides from the previous step - generatePasswordResetTokenPOST: async (input) => { - // Retrieve the email from the input - let email = input.formFields.find(i => i.id === "email")!.value as string; - - // check if user exists in SuperTokens - let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email - }, undefined, input.userContext); - let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; - }) - - if (emailPasswordUser === undefined) { - // User does not exist in SuperTokens - // Check if the user exists in the legacy provider and retrieve their data - let legacyUserData = await retrieveUserDataFromExternalProvider(email); - if (legacyUserData) { - // create a SuperTokens account for the user with a temporary password - let tempPassword = await generatePassword(); - let signupResponse = await EmailPassword.signUp(input.tenantId, email, tempPassword, undefined, input.userContext); - - if (signupResponse.status === "OK") { - - // If user is succesfully created we map the legacy id to their SuperTokens Id. - await SuperTokens.createUserIdMapping({ - superTokensUserId: signupResponse.user.id, - externalUserId: legacyUserData.user_id - }) - signupResponse.user.id = legacyUserData.user_id - signupResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserData.user_id); - signupResponse.recipeUserId = new RecipeUserId(legacyUserData.user_id); - - // We also need to set the email verification status of the user - if (legacyUserData.isEmailVerified) { - // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signupResponse.recipeUserId, email, input.userContext); - - if (generateEmailVerificationTokenResponse.status === "OK") { - // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); - } - } - - // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe - UserMetadata.updateUserMetadata(legacyUserData.user_id, { isUsingTemporaryPassword: true }) - } else { - throw new Error("Should never come here") - } - } - } - return await originalImplementation.generatePasswordResetTokenPOST!(input); - }, +import { randomBytes } from "node:crypto"; + +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import EmailVerification from "supertokens-node/recipe/emailverification"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import Session from "supertokens-node/recipe/session"; +const emailPasswordRecipe = EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + // Add overrides from the previous step + generatePasswordResetTokenPOST: async (input) => { + // Retrieve the email from the input + let email = input.formFields.find((i) => i.id === "email")!.value as string; + + // check if user exists in SuperTokens + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( + input.tenantId, + { + email, + }, + undefined, + input.userContext, + ); + let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { + return ( + u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined + ); + }); + + if (emailPasswordUser === undefined) { + // User does not exist in SuperTokens + // Check if the user exists in the legacy provider and retrieve their data + let legacyUserData = await retrieveUserDataFromExternalProvider(email); + if (legacyUserData) { + // create a SuperTokens account for the user with a temporary password + let tempPassword = await generatePassword(); + let signupResponse = await EmailPassword.signUp( + input.tenantId, + email, + tempPassword, + undefined, + input.userContext, + ); + + if (signupResponse.status === "OK") { + // If the user is successfully created, map the legacy ID to the SuperTokens ID. + const mappingResponse = await SuperTokens.createUserIdMapping({ + superTokensUserId: signupResponse.user.id, + externalUserId: legacyUserData.user_id, + userContext: input.userContext, + }); + if (mappingResponse.status !== "OK") { + throw new Error("Legacy user ID conflicts with an existing mapping"); + } + // We also need to set the email verification status of the user + if (legacyUserData.isEmailVerified) { + // Generate an email verification token for the user + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( + input.tenantId, + signupResponse.recipeUserId, + email, + input.userContext, + ); + + if (generateEmailVerificationTokenResponse.status === "OK") { + // Verify the user's email + await EmailVerification.verifyEmailUsingToken( + input.tenantId, + generateEmailVerificationTokenResponse.token, + undefined, + input.userContext, + ); + } + } + + // We also need to identify that the user is using a temporary password. We do through the userMetadata recipe + await UserMetadata.updateUserMetadata(signupResponse.user.id, { isUsingTemporaryPassword: true }); + } else { + throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); + } } - } - } -}) + } + return await originalImplementation.generatePasswordResetTokenPOST!(input); + }, + }; + }, + }, +}); + +SuperTokens.init({ + framework: "express", + supertokens: { connectionURI: "" }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" }), UserMetadata.init()], +}); async function generatePassword(): Promise { - // TODO: generate a random password - return "" + return randomBytes(32).toString("base64url"); } -async function retrieveUserDataFromExternalProvider(email: string): Promise<{ - user_id: string, - isEmailVerified: boolean -} | undefined> { - // TODO: retrieve user data if a user with the input email exists in the external provider. - return undefined; +async function retrieveUserDataFromExternalProvider(email: string): Promise< + | { + user_id: string; + isEmailVerified: boolean; + } + | undefined +> { + // TODO: retrieve user data if a user with the input email exists in the external provider. + return undefined; } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" +import secrets from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init @@ -663,7 +747,7 @@ from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) -from supertokens_python.recipe import emailpassword +from supertokens_python.recipe import emailpassword, emailverification, usermetadata, session from supertokens_python.recipe.emailpassword.asyncio import sign_up from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, @@ -679,7 +763,7 @@ from supertokens_python.recipe.emailverification.interfaces import ( CreateEmailVerificationTokenOkResult, ) from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata -from supertokens_python.types import RecipeUserId +from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput @@ -697,10 +781,10 @@ def override_emailpassword_apis(original_implementation: APIInterface): # retrieve the email from the form fields email = None for field in form_fields: - if field.id is "email": + if field.id == "email": email = field.value if email is None: - raise Exception("Should never come here") + raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Check if an email-password user with the input email exists in SuperTokens supertokens_user_with_same_email = await list_users_by_account_info( @@ -729,16 +813,14 @@ def override_emailpassword_apis(original_implementation: APIInterface): tenant_id, email, tempPassword, None, user_context ) if not isinstance(response, SignUpOkResult): - raise Exception("Should never come here") + raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the SuperTokens userId to the legacy userId - await create_user_id_mapping(response.user.id, legacy_user_data.user_id) - response.user.id = legacy_user_data.user_id - response.user.login_methods[0].recipe_user_id = RecipeUserId( - legacy_user_data.user_id + mapping_response = await create_user_id_mapping( + response.user.id, legacy_user_data.user_id, user_context=user_context ) - response.recipe_user_id = RecipeUserId(legacy_user_data.user_id) - + if not isinstance(mapping_response, CreateUserIdMappingOkResult): + raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status if legacy_user_data.isEmailVerified: # Generate an email verification token for the user @@ -761,7 +843,7 @@ def override_emailpassword_apis(original_implementation: APIInterface): # We also need to identify that the user is using a temporary password. We do through the userMetadata recipe await update_user_metadata( - legacy_user_data.user_id, {"isUsingTemporaryPassword": True} + response.user.id, {"isUsingTemporaryPassword": True}, user_context ) return await original_generate_password_reset_token_post( @@ -787,34 +869,37 @@ async def retrieve_user_data_from_external_provider( return None -async def generate_password(): - # TODO: generate a random password - return "" +async def generate_password() -> str: + return secrets.token_urlsafe(32) init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) - ) + ), + emailverification.init("OPTIONAL"), + usermetadata.init(), + session.init(), ], ) ``` - - - - - + + ```go import ( + "crypto/rand" + "encoding/base64" "errors" "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" + "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" + "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) @@ -843,7 +928,7 @@ func main() { } if email == nil { - return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("Should never come here") + return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } // Check if an email-password user with the input email exists in SuperTokens @@ -860,26 +945,30 @@ func main() { if legacyUserInfo != nil { // Create a SuperTokens account for the user with a temporary password - tempPassword := generatePassword() + tempPassword, err := generatePassword() + if err != nil { + return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err + } response, err := emailpassword.SignUp(tenantId, *email, tempPassword) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } if response.OK == nil { - return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("should never come here") + return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") } + recipeUserId := response.OK.User.ID // Map the external provider's userId to the SuperTokens userId - _, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil) + mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err } - // Set the userId in the response to use the provider's userId - response.OK.User.ID = legacyUserInfo.userId - + if mapping.OK == nil { + return epmodels.GeneratePasswordResetTokenPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") + } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { - generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &response.OK.User.Email) + generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &response.OK.User.Email) if err != nil { return epmodels.GeneratePasswordResetTokenPOSTResponse{}, err @@ -914,6 +1003,8 @@ func main() { }, }), usermetadata.Init(nil), + emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), + session.Init(nil), }, }) } @@ -928,15 +1019,16 @@ func retrieveUserDataFromExternalProvider(email string) *ExternalUserInfo { return nil } -func generatePassword() string { - // TODO: generate a random password - return "" +func generatePassword() (string, error) { + password := make([]byte, 32) + if _, err := rand.Read(password); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(password), nil } ``` - - - - + + The code above overrides the `generatePasswordResetTokenPOST` API. This is the first step in the password reset flow and is responsible for generating the password reset token to be sent with the reset password email. @@ -950,44 +1042,53 @@ The code above overrides the `generatePasswordResetTokenPOST` API. This is the f If the password reset flow is successfully completed we need to check if the user has `isUsingTemporaryPassword` set in their metadata and remove it if it exists. - - - + + ```tsx -import EmailPassword from "supertokens-node/recipe/emailpassword" -import UserMetadata from "supertokens-node/recipe/usermetadata" - -EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - // TODO: implementation details in previous step - passwordResetPOST: async function (input) { - let response = await originalImplementation.passwordResetPOST!(input); - if (response.status === "OK") { - let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext) - if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { - // Since the password reset we can remove the isUsingTemporaryPassword flag - await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }) - } - } - return response - } +import SuperTokens from "supertokens-node"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import Session from "supertokens-node/recipe/session"; +const emailPasswordRecipe = EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + // TODO: implementation details in previous step + passwordResetPOST: async function (input) { + let response = await originalImplementation.passwordResetPOST!(input); + if (response.status === "OK") { + let usermetadata = await UserMetadata.getUserMetadata(response.user.id, input.userContext); + if (usermetadata.status === "OK" && usermetadata.metadata.isUsingTemporaryPassword) { + // Since the password reset we can remove the isUsingTemporaryPassword flag + await UserMetadata.updateUserMetadata(response.user.id, { isUsingTemporaryPassword: null }); } - } - } -}) + } + return response; + }, + }; + }, + }, +}); + +SuperTokens.init({ + framework: "express", + supertokens: { connectionURI: "" }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [emailPasswordRecipe, Session.init(), UserMetadata.init()], +}); ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from typing import Any, Dict, List from supertokens_python import InputAppInfo, init -from supertokens_python.recipe import emailpassword +from supertokens_python.recipe import emailpassword, usermetadata, session from supertokens_python.recipe.emailpassword.interfaces import ( APIInterface, APIOptions, @@ -1036,24 +1137,24 @@ def override_emailpassword_apis(original_implementation: APIInterface): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) - ) + ), + usermetadata.init(), + session.init(), ], ) ``` - - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" + "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) @@ -1101,14 +1202,13 @@ func main() { }, }), usermetadata.Init(nil), + session.Init(nil), }, }) } ``` - - - - + + The code above overrides the `passwordResetPOST` API and is a continuation of the password reset flow: @@ -1119,115 +1219,154 @@ The code above overrides the `passwordResetPOST` API and is a continuation of th Apart from the changes we made in Step 1, we also need to account for users who have initiated a password reset but have not completed the flow. We need to handle two cases: - Prevent sign in from accounts that have temporary passwords. - If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked. -- If a user initiates a password reset but remembers their password, they should be able to sign in. +- If a user initiates a password reset but remembers their password, they should be able to sign in. - In this case the user should be able to login and the database should be updated to reflect the new password. - - - + + ```tsx import SuperTokens from "supertokens-node"; -import EmailPassword from "supertokens-node/recipe/emailpassword" +import EmailPassword from "supertokens-node/recipe/emailpassword"; import EmailVerification from "supertokens-node/recipe/emailverification"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -import { RecipeUserId } from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +const emailPasswordRecipe = EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInPOST: async function (input) { + // Check if an email-password user with the input email exists in SuperTokens + let email = input.formFields.find((field) => field.id === "email")!.value as string; + let password = input.formFields.find((field) => field.id === "password")!.value as string; + let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo( + input.tenantId, + { + email: email, + }, + undefined, + input.userContext, + ); + let emailPasswordUser = supertokensUsersWithSameEmail.find((u) => { + return ( + u.loginMethods.find((lM) => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined + ); + }); + + if (emailPasswordUser === undefined) { + // EmailPassword user with the input email does not exist in SuperTokens + // Check if the input credentials are valid in the external provider + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); + if (legacyUserInfo === undefined) { + // credentials are incorrect + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } -EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInPOST: async function (input) { - // Check if an email-password user with the input email exists in SuperTokens - let email = input.formFields.find((field) => field.id === "email")!.value as string; - let password = input.formFields.find((field) => field.id === "password")!.value as string; - let supertokensUsersWithSameEmail = await SuperTokens.listUsersByAccountInfo(input.tenantId, { - email: email - }, undefined, input.userContext); - let emailPasswordUser = supertokensUsersWithSameEmail.find(u => { - return u.loginMethods.find(lM => lM.hasSameEmailAs(email) && lM.recipeId === "emailpassword") !== undefined; - }) - - if (emailPasswordUser === undefined) { - // EmailPassword user with the input email does not exist in SuperTokens - // Check if the input credentials are valid in the external provider - let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password) - if (legacyUserInfo === undefined) { - // credentials are incorrect - return { - status: "WRONG_CREDENTIALS_ERROR" - } - } - - // Call the signup function to create a new SuperTokens user. - let signUpResponse = await EmailPassword.signUp(input.tenantId, email, password, undefined, input.userContext); - if (signUpResponse.status !== "OK") { - throw new Error("Should never come here") - } - - // Map the external provider's userId to the SuperTokens userId - await SuperTokens.createUserIdMapping({ superTokensUserId: signUpResponse.user.id, externalUserId: legacyUserInfo.user_id }) - // Set the userId in the response to use the provider's userId - signUpResponse.user.id = legacyUserInfo.user_id - signUpResponse.user.loginMethods[0].recipeUserId = new RecipeUserId(legacyUserInfo.user_id); - - // We also need to set the email verification status of the user - if (legacyUserInfo.isEmailVerified) { - // Generate an email verification token for the user - let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken(input.tenantId, signUpResponse.recipeUserId, email, input.userContext); - - if (generateEmailVerificationTokenResponse.status === "OK") { - // Verify the user's email - await EmailVerification.verifyEmailUsingToken("public", generateEmailVerificationTokenResponse.token, undefined, input.userContext); - } - } - emailPasswordUser = signUpResponse.user; - } - // highlight-start - // Check if the user signing in has a temporary password - let userMetadata = await UserMetadata.getUserMetadata(emailPasswordUser.id, input.userContext) - if (userMetadata.status === "OK" && userMetadata.metadata.isUsingTemporaryPassword) { - // Check if the input credentials are valid in the external provider - let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); - if (legacyUserInfo) { - let loginMethod = emailPasswordUser.loginMethods.find(lM => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email)); - // Update the user's password with the correct password - EmailPassword.updateEmailOrPassword({ - recipeUserId: loginMethod!.recipeUserId, - password: password, - applyPasswordPolicy: false - }) - - // Update the user's metadata to remove the isUsingTemporaryPassword flag - UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }) - } else { - return { - status: "WRONG_CREDENTIALS_ERROR" - } - } - } - // highlight-end - - return originalImplementation.signInPOST!(input) - }, + // Call the signup function to create a new SuperTokens user. + let signUpResponse = await EmailPassword.signUp( + input.tenantId, + email, + password, + undefined, + input.userContext, + ); + if (signUpResponse.status !== "OK") { + throw new Error("Sign-up failed; re-read and reconcile this identity before retrying"); + } + + // Map the external provider's userId to the SuperTokens userId + const mappingResponse = await SuperTokens.createUserIdMapping({ + superTokensUserId: signUpResponse.user.id, + externalUserId: legacyUserInfo.user_id, + userContext: input.userContext, + }); + if (mappingResponse.status !== "OK") { + throw new Error("Legacy user ID conflicts with an existing mapping"); + } + // We also need to set the email verification status of the user + if (legacyUserInfo.isEmailVerified) { + // Generate an email verification token for the user + let generateEmailVerificationTokenResponse = await EmailVerification.createEmailVerificationToken( + input.tenantId, + signUpResponse.recipeUserId, + email, + input.userContext, + ); + + if (generateEmailVerificationTokenResponse.status === "OK") { + // Verify the user's email + await EmailVerification.verifyEmailUsingToken( + input.tenantId, + generateEmailVerificationTokenResponse.token, + undefined, + input.userContext, + ); + } } + emailPasswordUser = signUpResponse.user; + } + // Check if the user signing in has a temporary password + let userMetadata = await UserMetadata.getUserMetadata(emailPasswordUser.id, input.userContext); + if (userMetadata.status === "OK" && userMetadata.metadata.isUsingTemporaryPassword) { + // Check if the input credentials are valid in the external provider + let legacyUserInfo = await validateAndGetUserInfoFromExternalProvider(email, password); + if (legacyUserInfo) { + let loginMethod = emailPasswordUser.loginMethods.find( + (lM) => lM.recipeId === "emailpassword" && lM.hasSameEmailAs(email), + ); + // Update the user's password with the correct password + await EmailPassword.updateEmailOrPassword({ + recipeUserId: loginMethod!.recipeUserId, + password: password, + applyPasswordPolicy: false, + }); + + // Update the user's metadata to remove the isUsingTemporaryPassword flag + await UserMetadata.updateUserMetadata(emailPasswordUser.id, { isUsingTemporaryPassword: null }); + } else { + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + } + + return originalImplementation.signInPOST!(input); }, + }; + }, + }, +}); + +SuperTokens.init({ + framework: "express", + supertokens: { connectionURI: "" }, + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + }, + recipeList: [emailPasswordRecipe, Session.init(), EmailVerification.init({ mode: "OPTIONAL" }), UserMetadata.init()], +}); + +async function validateAndGetUserInfoFromExternalProvider( + email: string, + password: string, +): Promise< + | { + user_id: string; + isEmailVerified: boolean; } -}) - -async function validateAndGetUserInfoFromExternalProvider(email: string, password: string): Promise<{ - user_id: string, - isEmailVerified: boolean -} | undefined> { - // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. - return undefined + | undefined +> { + // TODO: Validate the input credentials against the external authentication provider. If the credentials are valid return the user info. + return undefined; } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from typing import Any, Dict, List, Union from supertokens_python import InputAppInfo, init @@ -1235,7 +1374,7 @@ from supertokens_python.asyncio import ( create_user_id_mapping, list_users_by_account_info, ) -from supertokens_python.recipe import emailpassword +from supertokens_python.recipe import emailpassword, emailverification, usermetadata, session from supertokens_python.recipe.emailpassword.asyncio import ( sign_up, update_email_or_password, @@ -1259,7 +1398,7 @@ from supertokens_python.recipe.usermetadata.asyncio import ( get_user_metadata, update_user_metadata, ) -from supertokens_python.types import RecipeUserId +from supertokens_python.interfaces import CreateUserIdMappingOkResult from supertokens_python.types.base import AccountInfoInput @@ -1310,17 +1449,14 @@ def override_emailpassword_apis(original_implementation: APIInterface): # Call the sign_up function to create a new SuperTokens user. response = await sign_up(tenant_id, email, password, None, user_context) if not isinstance(response, SignUpOkResult): - raise Exception("Should never come here") + raise Exception("Sign-up failed; re-read and reconcile this identity before retrying") # Map the external provider's userId to the SuperTokens userId - await create_user_id_mapping(response.user.id, legacy_user_info.user_id) - # Set the userId in the response to use the provider's userId - response.user.id = legacy_user_info.user_id - response.user.login_methods[0].recipe_user_id = RecipeUserId( - legacy_user_info.user_id + mapping_response = await create_user_id_mapping( + response.user.id, legacy_user_info.user_id, user_context=user_context ) - response.recipe_user_id = RecipeUserId(legacy_user_info.user_id) - + if not isinstance(mapping_response, CreateUserIdMappingOkResult): + raise Exception("Legacy user ID conflicts with an existing mapping") # We also need to set the email verification status of the user if legacy_user_info.isEmailVerified: # Generate an email verification token for the user @@ -1344,7 +1480,6 @@ def override_emailpassword_apis(original_implementation: APIInterface): ) emailpassword_user = response.user - # highlight-start # Check if the user signing in has a temporary password metadata_result = await get_user_metadata(emailpassword_user.id) if ( @@ -1381,7 +1516,6 @@ def override_emailpassword_apis(original_implementation: APIInterface): ) else: return WrongCredentialsError() - # highlight-end return await original_emailpassword_sign_in( form_fields, @@ -1410,19 +1544,19 @@ async def validate_and_get_user_info_from_external_provider( init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ emailpassword.init( override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis) - ) + ), + emailverification.init("OPTIONAL"), + usermetadata.init(), + session.init(), ], ) ``` - - - - - + + ```go import ( "errors" @@ -1430,7 +1564,9 @@ import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" + "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" "github.com/supertokens/supertokens-golang/recipe/usermetadata" + "github.com/supertokens/supertokens-golang/recipe/session" "github.com/supertokens/supertokens-golang/supertokens" ) @@ -1485,19 +1621,23 @@ func main() { return epmodels.SignInPOSTResponse{}, err } - if response.OK == nil { - return epmodels.SignInPOSTResponse{}, errors.New("Should never come here") - } - - // Map the external provider's userId to the SuperTokens userId - _, err = supertokens.CreateUserIdMapping(response.OK.User.ID, legacyUserInfo.userId, nil, nil) - // Set the userId in the response to use the provider's userId - response.OK.User.ID = legacyUserInfo.userId + if response.OK == nil { + return epmodels.SignInPOSTResponse{}, errors.New("sign-up failed; re-read and reconcile this identity before retrying") + } + recipeUserId := response.OK.User.ID + // Map the external provider's userId to the SuperTokens userId + mapping, err := supertokens.CreateUserIdMapping(recipeUserId, legacyUserInfo.userId, nil, nil) + if err != nil { + return epmodels.SignInPOSTResponse{}, err + } + if mapping.OK == nil { + return epmodels.SignInPOSTResponse{}, errors.New("legacy user ID mapping conflicts with an existing mapping") + } // We also need to set the email verification status of the user if legacyUserInfo.isEmailVerified { // Generate an email verification token for the user - generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, response.OK.User.ID, &email) + generateEmailVerificationTokenResponse, err := emailverification.CreateEmailVerificationToken(tenantId, recipeUserId, &email) if err != nil { return epmodels.SignInPOSTResponse{}, err } @@ -1555,6 +1695,8 @@ func main() { }, }), usermetadata.Init(nil), + emailverification.Init(evmodels.TypeInput{Mode: evmodels.ModeOptional}), + session.Init(nil), }, }) } @@ -1569,10 +1711,8 @@ func validateAndGetUserInfoFromExternalProvider(email string, password string) * return nil } ``` - - - - + + The code above adds the following changes to the `signInPOST` API: @@ -1587,10 +1727,10 @@ This strategy takes into account the following edge cases to ensure a smooth mig - The changes proposed in Step 3 and Step 4 resolve the this edge case. - User starts the password reset flow and attempts to sign in with a temporary password - If, for any reason, the user tries to sign into their account with the temporary password, then the login method should be blocked. - - The changes proposed in Step 5 allows for this flow + - The changes proposed in Step 5 allows for this flow - User starts the password reset flow but they remember their password and try to login with the valid password. - In this scenario if the user starts the password reset flow, a new SuperTokens account with a temporary password is created. Instead of completing the password reset flow they remember their password and try to sign in. - In this case the user should be able to successfully sign in and the account should be updated with the valid password. + In this case the user should be able to successfully sign in and the account should be updated with the valid password. - The changes proposed in Step 5 allows for this flow. ## When can I stop using my legacy authentication provider? diff --git a/docs/migration/legacy/account-creation/meta.ts b/docs/migration/legacy/account-creation/meta.ts new file mode 100644 index 0000000000..0b9f996540 --- /dev/null +++ b/docs/migration/legacy/account-creation/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Step 1: Account Creation", + icon: "user-check", + order: 2, +}); diff --git a/docs/migration/legacy/account-creation/user-creation.mdx b/docs/migration/legacy/account-creation/user-creation.mdx index c639d95243..0b0315438d 100644 --- a/docs/migration/legacy/account-creation/user-creation.mdx +++ b/docs/migration/legacy/account-creation/user-creation.mdx @@ -1,31 +1,41 @@ --- -id: user-creation title: User Creation -hide_title: true -sidebar_position: 1 -page_type: guide -category: migration +sidebar: + order: 1 --- +## Email Password Migration -# User Creation +:::caution[Legacy procedure] +This page documents version-qualified CDI 2.16 behavior. For a new migration, use the current +[account migration guide](/migration/account-migration), which supports staged bulk import and reconciliation. +::: -## Email Password Migration +:::danger[Secure the legacy Core before importing] +Keep Core on a private network; for a same-host migration, bind the published port to the local interface only. Generate a +high-entropy API key (for example, `openssl rand -hex 32`), store it in your secret manager, and configure the same key in +Core and the migration client without putting it in source control, shell history, or logs. Pin the Core/database image +to a tested immutable digest that implements the documented CDI/storage version. Never use an untagged or mutable image +for a migration, and do not expose Core directly to the internet. -:::important +The requests below read their API-key header from `/run/secrets/supertokens-curl.conf`. Create it with mode `0600` and +the line `header = "api-key: "`; provision it from your secret manager and disable shell/curl tracing. +::: + +:::note If you do not have access to your user's password hashes, you can use our [guide for migrating them dynamically during login](./ep-migration-without-password-hash). ::: SuperTokens allows you to import users with password hashes generated with `BCrypt`, `Argon2` and `Firebase SCrypt` with our import user API. You can find the API spec [here](https://app.swaggerhub.com/apis/supertokens/CDI/2.16.0#/EmailPassword%20Recipe/userImport). -### Migrating users With Argon2 or `BCrypt` Password hashes +### Migrating users with Argon2 or `BCrypt` password hashes For users with `BCrypt` or `Argon2` password hashes you can use the following curl command to import your user. ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/user/passwordhash/import' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/user/passwordhash/import' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com", @@ -33,8 +43,7 @@ curl --location --request POST '^{coreInfo.uri}/recipe/user/passwordhash/import' }' ``` -:::important -SuperTokens accepts `BCrypt` and `Argon2` hashes in standard format. When exporting password hashes from authentication providers the structure might be changed. For example, Auth0 adds an identifier to the exported password hashes which needs to be removed before importing into SuperTokens. +:::note[SuperTokens accepts `BCrypt` and `Argon2` hashes in standard format. When exporting password hashes from authentication providers the structure might be changed. For example, Auth0 adds an identifier to the exported password hashes which needs to be removed before importing into SuperTokens.] Sample password hashes for `BCrypt` and Argon2 in standard format: @@ -45,47 +54,46 @@ Sample password hashes for `BCrypt` and Argon2 in standard format: ### Migrating users with Firebase `SCrypt` Password hashes -Importing users from Firebases requires an update to your SuperTokens core config and formatting the input password hash. +Importing users from Firebase requires an update to your SuperTokens Core configuration and formatting the input password hash. #### Step 1: Retrieve your Firebase password hashing parameters from your dashboard. -Firebase password hashing details modal +Firebase password hashing details modal #### Step 2: Update the SuperTokens core to use the `base64_signer_key` -- ** For Managed Service ** +**For Managed Service** - Edit the core configuration on the **Configuration** page of the relevant deployment in the SuperTokens SaaS Dashboard. - Set the `firebase_password_hashing_signer_key` field in the config to the `base64_signer_key` retrieved from your firebase hashing parameters. +**With Docker:** Create `/run/secrets/supertokens-migration.environment` with mode `0600`. It must contain +`API_KEYS=` and `FIREBASE_PASSWORD_HASHING_SIGNER_KEY=`. Use deployment-managed +secrets instead of an environment file where available. +**Without Docker:** Restrict the Core listener to a private or local-only interface with host firewall/network policy. The +exact process binding is deployment-specific; verify from another host that port `3567` is unreachable before importing. - - - - + + ```bash - docker run \ - -p 3567:3567 \ - // highlight-next-line - -e FIREBASE_PASSWORD_HASHING_SIGNER_KEY="gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXdWscZx0l2WbCJ1wbg==" \ - -d supertokens/supertokens- +docker run \ + --env-file /run/secrets/supertokens-migration.environment \ + -p 127.0.0.1:3567:3567 \ + -d supertokens/supertokens-@sha256: ``` - - - - + + ```yaml # Add your base64_signer_key to the following in the config.yaml file. # The file path can be found by running the "supertokens --help" command firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXdWscZx0l2WbCJ1wbg==" +api_keys: "" ``` - - - - + + #### Step 3: SuperTokens requires firebase password hashes to be in a specific format to be parsed. @@ -98,9 +106,9 @@ firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXd "users": [ { "localId": "userId", - "email": "johnDoe@example.com" + "email": "johnDoe@example.com", "passwordHash": "9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==", - "salt": "/cj0jC1br5o4+w==", + "salt": "/cj0jC1br5o4+w==" } ] } @@ -109,12 +117,12 @@ firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXd The memory cost, rounds and salt separator retrieved from the password hashing config are: ```json { - mem_cost: 14, - rounds: 8, - base64_salt_separator: "Bw==" + "mem_cost": 14, + "rounds": 8, + "base64_salt_separator": "Bw==" } ``` - + The password hash would be the following: `$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==` The example password hash is in the following format `$f_scrypt$$$m=$r=$s=` @@ -122,9 +130,9 @@ firebase_password_hashing_signer_key: "gRhC3eDeQOdyEn4bMd9c6kxguWVmcIVq/HbJKnCXd #### Step 4: Run the following `curl` command to import the user ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/user/passwordhash/import' \ +curl --location --request POST '/recipe/user/passwordhash/import' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ ---header 'api-key: ^{coreInfo.key}' \ --data-raw '{ "email": "test@example.com", "passwordHash": "$f_scrypt$9Y8ICWcqbzmI42DxV1jpyEjbrJPG8EQ6nI6oC32JYz+/dd7aEjI/R7jG9P5kYh8v9gyqFKaXMDzMg7eLCypbOA==$/cj0jC1br5o4+w==$m=14$r=8$s=Bw==", @@ -135,15 +143,19 @@ curl --location --request POST '^{coreInfo.uri}/recipe/user/passwordhash/import' ## Passwordless Migration -To migrate a Passwordless user from your previous authentication provider to SuperTokens, you will first need to generate a code for the user and then call the consume code API. +This legacy procedure is an active passwordless authentication flow, not a side-effect-free import. Generating a code +creates temporary passwordless device/code records with an expiry. Consuming the link code consumes that credential and +performs passwordless sign-in/up, creating the user if necessary. Repeating or racing these requests can produce used, +expired, or duplicate-flow errors. Run them only in a controlled migration process, keep returned codes secret, and +reconcile the resulting user before retrying. Use bulk import for new migrations. ### Generate passwordless code **With Email** ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/signinup/code' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/signinup/code' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "email": "johndoe@example.com" @@ -153,8 +165,8 @@ curl --location --request POST '^{coreInfo.uri}/recipe/signinup/code' \ **With Phone Number** ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/signinup/code' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/signinup/code' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "phoneNumber": "+14155552671" @@ -165,14 +177,14 @@ On successfully generating the passwordless code you should see the following re ```json { - "status": "OK", - "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", - "codeId": "4fe93f8e-a5da-4588-82e2-314c6993b345", - "deviceId": "+cWm1Y2EFxEPyHM7CAwYyAdkakBeoEDm6IOGT3xfa1U=", - "userInputCode": "463152", - "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=", - "timeCreated": 1664283193059, - "codeLifetime": 900000 + "status": "OK", + "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", + "codeId": "4fe93f8e-a5da-4588-82e2-314c6993b345", + "deviceId": "+cWm1Y2EFxEPyHM7CAwYyAdkakBeoEDm6IOGT3xfa1U=", + "userInputCode": "463152", + "linkCode": "UlEb3-gbIYow61ce6RNzghkGN8qcHkpRwbhHbvMEjxY=", + "timeCreated": 1664283193059, + "codeLifetime": 900000 } ``` @@ -181,8 +193,8 @@ On successfully generating the passwordless code you should see the following re Retrieve the `preAuthSessionId` and `linkCode` from the previous response and set them as request body parameters for the consume code request. ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/signinup/code/consume' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/signinup/code/consume' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "preAuthSessionId": "d3Zpa9eoyV2Wr7uN5DLr6H1clzbwwGTc_0wIIXJT55M=", @@ -190,11 +202,12 @@ curl --location --request POST '^{coreInfo.uri}/recipe/signinup/code/consume' \ }' ``` -If the user has both email and password associated with them, then you can call the update user API to associate the missing information +If the imported passwordless login method should have both an email address and phone number, use its returned user ID +to add the missing contact information: ```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/user' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/recipe/user' \ +--config /run/secrets/supertokens-curl.conf \ --header 'rid: passwordless' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ @@ -211,11 +224,11 @@ To migrate users with social accounts we can simply call the SuperTokens Core's For example: -If we were importing a user with Google as their provider with their third party userId being `106347997792363870000`, we can run the following curl command to import the user. +If we were importing a user with Google as their provider with their third party userId being `106347997792363870000`, we can run the following curl command to import the user. ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/signinup' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/signinup' \ +--config /run/secrets/supertokens-curl.conf \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "thirdPartyId": "google", @@ -231,10 +244,10 @@ curl --location --request POST '^{coreInfo.uri}/recipe/signinup' \ ## See also - - - - - - - + + + + + + + diff --git a/docs/migration/legacy/account-creation/user-id-mapping.mdx b/docs/migration/legacy/account-creation/user-id-mapping.mdx index 098e4f0c06..8e74ac9755 100644 --- a/docs/migration/legacy/account-creation/user-id-mapping.mdx +++ b/docs/migration/legacy/account-creation/user-id-mapping.mdx @@ -1,21 +1,16 @@ --- -id: user-id-mapping title: UserId Mapping -hide_title: true -sidebar_position: 2 -page_type: guide -category: migration +sidebar: + order: 2 --- -# UserId Mapping - UserId Mapping allows you to map existing userIds (from your old auth provider) to the SuperTokens userIds. This prevents you from having to update the existing `userIDs` in your application's table. As an example, if after creating the user in SuperTokens, their userId is `fa7a0841-b533-4478-95533-0fde890c3483` and the existing userId for that user is `customUserId`, then you can map these user IDs by calling the following API: ```bash -curl --location --request POST '^{coreInfo.uri}/recipe/userid/map' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request POST '/recipe/userid/map' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "superTokensUserId": "fa7a0841-b533-4478-95533-0fde890c3483", @@ -25,6 +20,6 @@ curl --location --request POST '^{coreInfo.uri}/recipe/userid/map' \ Now whenever this user signs in, or if you fetch information about this user from SuperTokens, their userID will be `customUserId`. -:::info Note +:::info[Note] The maximum allowed size of the `externalUserId` is 128 characters. ::: diff --git a/docs/migration/legacy/data-migration.mdx b/docs/migration/legacy/data-migration.mdx index d453597823..1f58c347fe 100644 --- a/docs/migration/legacy/data-migration.mdx +++ b/docs/migration/legacy/data-migration.mdx @@ -1,14 +1,9 @@ --- -id: data-migration title: Step 2. User Data Migration -hide_title: true -sidebar_position: 3 -page_type: tutorial -category: migration +sidebar: + order: 3 --- -# User Data Migration - Once your user accounts have been migrated over to SuperTokens, additional information like metadata, roles and permissions can be associated with the user. ## User Metadata Migration @@ -19,13 +14,13 @@ In this example we want to store the following metadata against our user: ```json { - "someKey": "someValue" + "someKey": "someValue" } ``` ```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/user/metadata' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/recipe/user/metadata' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "userId": "5acb2dbc-04f0-4c80-822a-7f06cd658f6f", @@ -42,8 +37,8 @@ SuperTokens allows you to assign roles and permissions to a userId. In this example we will be assigning the `admin` role to a user: ```bash -curl --location --request PUT '^{coreInfo.uri}/recipe/user/role' \ ---header 'api-key: ^{coreInfo.key}' \ +curl --location --request PUT '/recipe/user/role' \ +--header 'api-key: ' \ --header 'Content-Type: application/json; charset=utf-8' \ --data-raw '{ "role": "admin", @@ -52,18 +47,18 @@ curl --location --request PUT '^{coreInfo.uri}/recipe/user/role' \ }' ``` -:::info Important -Roles and permissions must be created before they can be assigned to a user. You can follow this [guide](/docs/additional-verification/user-roles/initial-setup) on creating roles and permissions in SuperTokens. +:::info[Important] +Roles and permissions must be created before they can be assigned to a user. You can follow this [guide](/additional-verification/user-roles/initial-setup) on creating roles and permissions in SuperTokens. ::: --- ## See also - - - - - - - + + + + + + + diff --git a/docs/migration/legacy/meta.ts b/docs/migration/legacy/meta.ts new file mode 100644 index 0000000000..e5ed3bd2e3 --- /dev/null +++ b/docs/migration/legacy/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Legacy Method", + icon: "history", + order: 4, + collapsed: true, +}); diff --git a/docs/migration/legacy/mfa-migration.mdx b/docs/migration/legacy/mfa-migration.mdx index f84b4074d5..4a9a0191c1 100644 --- a/docs/migration/legacy/mfa-migration.mdx +++ b/docs/migration/legacy/mfa-migration.mdx @@ -1,12 +1,7 @@ --- -id: mfa-migration title: Step 4. MFA migration -hide_title: true -sidebar_position: 5 -page_type: guide -category: migration +sidebar: + order: 5 --- -# MFA migration - -If you are using MFA in your app, checkout the MFA migration section [here](/docs/additional-verification/mfa/migration/legacy-to-new) after you have gone through the previous steps in migration. +If you are using MFA in your app, checkout the MFA migration section [here](/additional-verification/mfa/migration/legacy-to-new) after you have gone through the previous steps in migration. diff --git a/docs/migration/legacy/session-migration.mdx b/docs/migration/legacy/session-migration.mdx index b4e59cfd26..5d3f1a062b 100644 --- a/docs/migration/legacy/session-migration.mdx +++ b/docs/migration/legacy/session-migration.mdx @@ -1,15 +1,10 @@ --- -id: session-migration title: Step 3. Session Migration -hide_title: true -sidebar_position: 4 -page_type: guide -category: migration +sidebar: + order: 4 --- -# Session Migration - -In this section we will go over how to migrate your user sessions from you previous authentication provider to SuperTokens. +This section explains how to migrate user sessions from your previous authentication provider to SuperTokens. This process involves two steps. - Adding a new `/migrate-session` API to your backend which will create a new SuperTokens session @@ -17,60 +12,169 @@ This process involves two steps. ## Flow -Session migration flow chart +Session migration flow chart -### Backend Changes: +### Backend changes -- Create an API on your backend, this will be called by the frontend to migrate a user's existing session to a SuperTokens session: +Create a rate-limited backend API that exchanges a valid legacy access token for a SuperTokens session. The following +example uses the APIs released in SuperTokens Node SDK 24.0.3. -```tsx title="Backend changes" +```tsx title="Backend changes" check=false reason="Requires surrounding framework application context" import express from "express"; +import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -let app = express(); - -app.post("/migrate-session", async (req, res) => { +const app = express(); +app.use(express.json()); - // extract the access token from the request object - if(req.headers.authorization !== undefined){ - let access_token = req.headers.authorization.split("Bearer ")[1]; - - // verify the access token and retrieve the old userId - let customUserId = await verifyAccessTokenAndRetriveUserId(access_token); +interface VerifiedLegacyToken { + issuer: string; + subject: string; + tenantId: string; +} - // create a new SuperTokens session using the customUserId - // the createNewSession function will attach the SuperTokens session tokens to the response object. - // @ts-ignore - await Session.createNewSession(req, res, customUserId) - res.send({ - status: "OK" - }) +app.post("/migrate-session", migrationRateLimiter, async (req, res, next) => { + const match = req.headers.authorization?.match(/^Bearer[ \t]+([^\s,]+)$/i); + const idempotencyKey = req.header("Idempotency-Key"); + if (match === null || match === undefined || !isValidIdempotencyKey(idempotencyKey)) { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; + } + + let verifiedToken: VerifiedLegacyToken; + try { + verifiedToken = await verifyLegacyAccessToken(match[1]); + } catch { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; + } + + try { + const identity = getNamespacedLegacyIdentity(verifiedToken); + await enforceVerifiedIdentityRateLimit(identity.identityKey); + + const mapping = await SuperTokens.getUserIdMapping({ + userId: identity.externalUserId, + userIdType: "EXTERNAL", + }); + if (mapping.status !== "OK") { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; } - // handle access_token not present in request - -}) - -async function verifyAccessTokenAndRetriveUserId(access_token: string): Promise { - // verify the access_token and return the decoded userId - return "..."; + const recipeUserId = SuperTokens.convertToRecipeUserId(mapping.superTokensUserId); + const result = await migrateSessionIdempotently( + { + idempotencyKey, + identityKey: identity.identityKey, + tenantId: verifiedToken.tenantId, + recipeUserId: recipeUserId.getAsString(), + }, + () => Session.createNewSession(req, res, verifiedToken.tenantId, recipeUserId), + ); + if (result.status === "CONFLICT") { + res.status(409).send({ status: "MIGRATION_CONFLICT" }); + return; + } + if (result.status === "IN_PROGRESS") { + res.set("Retry-After", "1").status(409).send({ status: "MIGRATION_IN_PROGRESS" }); + return; + } + res.send({ status: result.status }); + } catch (error) { + next(error); + } +}); + +app.post("/confirm-session-migration", migrationRateLimiter, async (req, res, next) => { + const idempotencyKey = req.body?.idempotencyKey; + if (!isValidIdempotencyKey(idempotencyKey)) { + res.status(400).send({ status: "INVALID_IDEMPOTENCY_KEY" }); + return; + } + + try { + const session = await Session.getSession(req, res); + const confirmed = await confirmMigrationOutcome({ + idempotencyKey, + tenantId: session.getTenantId(), + recipeUserId: session.getRecipeUserId().getAsString(), + }); + res.status(confirmed ? 200 : 409).send({ status: confirmed ? "CONFIRMED" : "IDENTITY_MISMATCH" }); + } catch (error) { + next(error); + } +}); + +function isValidIdempotencyKey(value: unknown): value is string { + return typeof value === "string" && /^[A-Za-z0-9_-]{32,128}$/.test(value); } +declare function migrationRateLimiter(req: express.Request, res: express.Response, next: express.NextFunction): void; +declare function enforceVerifiedIdentityRateLimit(identityKey: string): Promise; + +declare function getNamespacedLegacyIdentity(verifiedToken: VerifiedLegacyToken): { + identityKey: string; + externalUserId: string; +}; + +declare function migrateSessionIdempotently( + input: { + idempotencyKey: string; + identityKey: string; + tenantId: string; + recipeUserId: string; + }, + createSession: () => ReturnType, +): Promise<{ status: "CREATED" | "RECOVERED" | "CONFLICT" | "IN_PROGRESS" }>; + +declare function confirmMigrationOutcome(input: { + idempotencyKey: string; + tenantId: string; + recipeUserId: string; +}): Promise; + +// Implement this contract with your provider's supported SDK or a JWT library configured for that provider. +declare function verifyLegacyAccessToken(accessToken: string): Promise; ``` -:::info Important +:::info[Important] -Verifying the access token will require you to access your previous providers JWKS endpoint, which means that you will have to continue to use your previous auth provider even after switching to SuperTokens. +`verifyLegacyAccessToken` is an application-owned security boundary, not a provider-independent implementation. It must +verify the signature with the provider's trusted algorithms and keys, and validate the exact issuer, audience, expiry, +not-before time, token type or purpose, and required subject claims. If the provider uses JWKS, use HTTPS, cache keys, +and refresh them safely when keys rotate. Derive `tenantId` from trusted application configuration and validated claims, +never directly from request data that has not been verified. -If you want to immediately stop querying your previous authentication provider you can use the JWKS public keys and provide them as a secret to the JWT verification function. -You can follow our [guide](/docs/additional-verification/session-verification/protect-api-routes) on how to do this. +`getNamespacedLegacyIdentity` must use the same collision-resistant canonical encoding used during account import. Its +external ID and identity key must include the validated issuer, trusted tenant, and subject. Do not concatenate ambiguous +strings or map by subject alone: subjects are only unique within an issuer and can also overlap between tenants. ::: -### Frontend Changes: +Configure CORS on the backend with the exact frontend origin, `credentials: true`, and the `Authorization`, +`Content-Type`, and `Idempotency-Key` request headers explicitly allowed. Do not combine credentialed requests with +`Access-Control-Allow-Origin: *`. The endpoint's error handler should return a generic 401 for verification failures +without exposing token-validation details. -- On your frontend, on page load, check if a session with your previous authentication provider exists. -- If a session exists send a request to the `/migrate-session` API with the access token to create a new SuperTokens session. -- Revoke the old session. +Implement `migrateSessionIdempotently` with a distributed outcome store indexed uniquely by both the stable request key +and the legacy identity scoped to its issuer and tenant. A completed outcome records the tenant, recipe user ID, and +created session handle. A +retry for the same key and identity must return or safely recover that logical outcome; if the original session was not +delivered, revoke it before issuing a replacement. Reject a key bound to another identity, another key for an already +migrated identity, and concurrent in-progress exchanges. Never store the raw legacy token. Keep completed outcomes at +least until the legacy session and token can no longer be accepted. `confirmMigrationOutcome` must compare the current +SuperTokens tenant and recipe user ID with that stored outcome and atomically mark it confirmed. + +### Frontend changes + +On page load, obtain the legacy token and its stable request key. If a SuperTokens session exists, confirm that it +matches the stored migration outcome before cleaning up the legacy session. Otherwise, perform the idempotent exchange, +then confirm the identities. Never treat an HTTP success alone as proof that the current SuperTokens and legacy +identities match. The example uses SuperTokens Web JS 0.16.0 and assumes that the SDK is initialized. ```tsx title="Frontend changes" import axios from "axios"; @@ -79,41 +183,73 @@ import Session from "supertokens-web-js/recipe/session"; // Call this function on page load async function migrateUserSessions() { - let apiDomain = "..."; - // On page load retrieve the users access token if a session exists - let accessToken = await getAccessTokenFromOldProvider() - - if (accessToken !== undefined) { - - // send a request to your migrate session endpoint with the bearer token - await axios.post(`${apiDomain}/migrate-session`, { - headers: { - "Authorization": `Bearer ${accessToken}` - } - }) - await revokeSessionFromOldProvider() + const apiDomain = "..."; + const accessToken = await getAccessTokenFromOldProvider(); + if (accessToken === undefined) { + return; + } + + const idempotencyKey = await getOrCreateMigrationIdempotencyKey(); + + if (!(await Session.doesSessionExist())) { + await axios.post( + `${apiDomain}/migrate-session`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Idempotency-Key": idempotencyKey, + }, + withCredentials: true, + }, + ); + } + + if (!(await confirmMigratedIdentity(apiDomain, idempotencyKey))) { + return; + } + + await revokeSessionFromOldProvider(); + await clearMigrationIdempotencyKey(); +} + +async function confirmMigratedIdentity(apiDomain: string, idempotencyKey: string): Promise { + try { + const response = await axios.post( + `${apiDomain}/confirm-session-migration`, + { idempotencyKey }, + { withCredentials: true }, + ); + return response.data.status === "CONFIRMED"; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 409) { + return false; } + throw error; + } } async function getAccessTokenFromOldProvider(): Promise { - // Check if a session with your your previous provider exists and return the access_token. Return undefined otherwise - return "..." + // Return the provider's access token when its session exists, or undefined otherwise. + return "..."; } +// Persist one random key for this specific legacy provider session until cleanup succeeds. +declare function getOrCreateMigrationIdempotencyKey(): Promise; +declare function clearMigrationIdempotencyKey(): Promise; + async function revokeSessionFromOldProvider() { - // Revoke the session associated with the previous provider + // Revoke the session associated with the previous provider } - ``` - --- ## See also - - - - - - - + + + + + + + diff --git a/docs/migration/meta.ts b/docs/migration/meta.ts new file mode 100644 index 0000000000..dbaf1e8e0a --- /dev/null +++ b/docs/migration/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Migration", + icon: "arrow-right", + order: 70, + collapsed: true, +}); diff --git a/docs/migration/overview.mdx b/docs/migration/overview.mdx index b54432ec84..0bb3be0013 100644 --- a/docs/migration/overview.mdx +++ b/docs/migration/overview.mdx @@ -1,27 +1,20 @@ --- title: Overview -hide_title: true -sidebar_position: 1 -pagination_prev: null description: Get a high-level overview of the migration process. -page_type: overview -category: migration +sidebar: + order: 1 --- +The **Migration** section covers instructions on how you can move your authentication data from your current authentication solution to **SuperTokens** -# Overview - - - The **Migration** section covers instructions on how you can move your authentication data from your current authentication solution to **SuperTokens** - +--- - ## Before You Start -The migration steps are intended to be executed after you have configured your [initial **SuperTokens** integration](/docs/quickstart/introduction). +The migration steps are intended to be executed after you have configured your [initial **SuperTokens** integration](/quickstart). Hence, to complete any of the next instructions you will need the following: -- An existing application that has a working **SuperTokens** integration. You can follow the [quickstart guide](/docs/quickstart/introduction) for instructions on how to achieve this. +- An existing application that has a working **SuperTokens** integration. You can follow the [quickstart guide](/quickstart) for instructions on how to achieve this. - A **SuperTokens** managed service account or a self-hosted **SuperTokens Core** instance. More specifically, you will have to make HTTP requests to your `CORE_API_ENDPOINT` using an `API_KEY`. @@ -31,26 +24,21 @@ More specifically, you will have to make HTTP requests to your `CORE_API_ENDPOIN The entire migration process can be broken down into two steps. Both are required to achieve a seamless transition. -1. [**Account Migration**](/docs/migration/account-migration/) +1. [**Account Migration**](/migration/account-migration/) This is the main part of the migration flow. You will be importing your users, together with their account credentials, from the legacy authentication provider into **SuperTokens**. -2. [**Session Migration**](/docs/migration/session-migration) +2. [**Session Migration**](/migration/session-migration) This step prevents any users with active sessions from experiencing authentication issues. You will extend the **SuperTokens** functionality to create new sessions for users that have authenticated using your legacy provider. -:::info - -You need to keep using your legacy authentication provider until you have implemented all the migration steps. +:::info[You need to keep using your legacy authentication provider until you have implemented all the migration steps.] After that, you can go ahead and switch to **SuperTokens** in your production application. ::: -With that in mind, you can now move to the first step, the [**Account Migration**](/docs/migration/account-migration/) process. - - - +With that in mind, you can now move to the first step, the [**Account Migration**](/migration/account-migration/) process. diff --git a/docs/migration/rownd/_category_.json b/docs/migration/rownd/_category_.json deleted file mode 100644 index d071b7e3f8..0000000000 --- a/docs/migration/rownd/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Rownd Migration", - "collapsible": true, - "collapsed": true, - "customProps": {}, - "position": 5 -} diff --git a/docs/migration/rownd/meta.ts b/docs/migration/rownd/meta.ts new file mode 100644 index 0000000000..dad30f0d77 --- /dev/null +++ b/docs/migration/rownd/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Rownd Migration", + order: 5, + collapsed: true, +}); diff --git a/docs/migration/rownd/migration-steps.mdx b/docs/migration/rownd/migration-steps.mdx index 1d56a5d8da..a32d79a0ea 100644 --- a/docs/migration/rownd/migration-steps.mdx +++ b/docs/migration/rownd/migration-steps.mdx @@ -1,20 +1,14 @@ --- title: Migration Steps -hide_title: true -sidebar_position: 1 -toc_max_heading_level: 3 description: Understand the Rownd to SuperTokens migration process. -page_type: tutorial -category: migration +sidebar: + order: 1 --- -# Migration steps +Move supported Rownd users, sessions, and compatible authentication flows to **SuperTokens** with an assisted, +project-specific migration. - - Move your Rownd users, sessions, and compatible authentication flows to **SuperTokens** without any breaking changes - - - +--- ## Overview @@ -33,44 +27,76 @@ Compared to Rownd, in SuperTokens you have to: Each step of the migration process requires an existing SuperTokens backend integration in your application. The following steps give you an overview over the process. -For detailed instructions on how to integrate SDKs you can read [the full guide](/docs/migration/rownd/sdk-integration-guide). +For detailed instructions on how to integrate SDKs you can read [the full guide](/migration/rownd/sdk-integration-guide). -:::info no-title +:::info The migration flow is not designed to be self service. Please get in touch with the [SuperTokens team](mailto:support@supertokens.com) for assistance during the whole process. ::: +## Checkpoints and rollback plan + +Agree on these gates with the SuperTokens team before changing production traffic. Record evidence and an owner for each +gate; do not advance on partial success. + +1. **Baseline checkpoint:** Export a time-stamped inventory of expected Rownd users and enabled identity providers. + Record current client/plugin versions, routing, OAuth configuration, and session behavior. Keep the existing Rownd + deployment and configuration available for rollback. +2. **Lazy-migration gate:** Deploy only to a controlled cohort. Verify successful and rejected legacy-token session + bootstrap, sign-in/up, sign-out, refresh, profile reads/writes, and expected compatibility claims. Confirm failures do + not create duplicate SuperTokens users or sessions before expanding the cohort. +3. **Bulk checkpoint:** Freeze or account for writes during the export boundary. Reconcile every expected Rownd user to + exactly one intended SuperTokens identity, including tenant/provider mapping. Classify every missing, duplicate, + failed, and retried record; a total count alone cannot pass this gate. +4. **Cutover gate:** Capture a final delta and reconciliation report, stop configuration changes, and define the exact + traffic switch and rollback deadline. Test both newly created SuperTokens sessions and supported migrated legacy + sessions before increasing traffic gradually. +5. **Post-cutover checkpoint:** Monitor authentication errors, session bootstrap/refresh failures, duplicate identities, + and reconciliation drift. Keep Rownd authoritative and reversible until the agreed observation window and all + acceptance checks pass. + +If any gate fails, stop the migration, route traffic back to the previously verified Rownd clients/backend path, and stop +new writes to the SuperTokens migration path while investigating. Do not copy SuperTokens-only writes back into Rownd +without a separately tested reverse-data contract. Instead, preserve the failed-state evidence, identify writes made +after the checkpoint, and have the migration owners decide whether to replay them after correction. Revoke migration +credentials and retire Rownd only after final identity, provider, session, and failed-record reconciliation is approved. + ### 1. Lazy migration -Update your existing Rownd client libraries to the latest version. -With this you will enable the following behavior: after a successful sign-up, the SDK will call the migration endpoint exposed by the compatibility plugin. -This phase ensures that no users are missed during the bulk migration flow. -This is necessary since there is a time gap between when you export all your data for bulk import and when you go live with **SuperTokens**. +For released lazy-migration behavior, update supported Rownd React, iOS, and Android clients to the versions selected +with the SuperTokens team. Do not assume that every Rownd client or version implements this behavior. After a successful +sign-up, a supported SDK calls the migration endpoint exposed by the compatibility plugin. +This phase reduces the export-to-cutover gap, but does not prove that every user was migrated. Reconcile it with the +bulk snapshot and failed records because users can be created between export and cutover. ### 2. Bulk migration This step is performed by the SuperTokens team. Once you have deployed the updated Rownd client libraries and confirmed that the migration works, you can start the next step. -The migration will copy all your existing users from the Rownd account to the newly created SuperTokens core instance. +The plugin provides a released paginated Rownd-user migration path. Your migration plan must still prove that the +project-specific export snapshot covered every expected user and that every failed or retried record was reconciled; +deployment of the plugin alone does not prove completeness. ### 3. Cutover Replace the Rownd client SDKs with the SuperTokens Rownd-compatible clients. -The [SDK integration guide](/docs/migration/rownd/sdk-integration-guide) goes into detail on how to do this. +The [SDK integration guide](/migration/rownd/sdk-integration-guide) goes into detail on how to do this. After cutover, authentication traffic goes through SuperTokens while your application uses Rownd-compatible APIs. #### Compatibility model -The migration is designed to preserve the Rownd-facing integration surface where possible. -Frontend APIs such as `requestSignIn()`, `signOut()`, `getAccessToken()`, profile management, and signed-in state continue to be exposed by the SuperTokens Rownd SDKs. -Authentication UIs are preserved as well as the Rownd specific session claims. +Released compatibility clients preserve parts of the Rownd-facing integration surface, depending on platform and +version. Verified implementations expose APIs including `requestSignIn()`, `signOut()`, `getAccessToken()`, profile +management, and signed-in state; released backend plugins preserve Rownd identity metadata and compatibility session +claims. This is not a no-change guarantee: cutover requires backend/plugin deployment, client package changes, and may +require platform upgrades or OAuth reauthorization. Confirm every API, UI, claim, and platform your application uses in +a staging environment before cutover. ## Next steps -Continue with the [SDK Integration Guide](/docs/migration/rownd/sdk-integration-guide). The backend plugin must be deployed before clients can migrate sessions or use SuperTokens-backed Rownd flows. +Continue with the [SDK Integration Guide](/migration/rownd/sdk-integration-guide). The backend plugin must be deployed before clients can migrate sessions or use SuperTokens-backed Rownd flows. After the backend is working, use the same guide to configure your frontend or mobile client platform. - diff --git a/docs/migration/rownd/sdk-integration-guide.mdx b/docs/migration/rownd/sdk-integration-guide.mdx index 646d684cf3..80b8fb1bff 100644 --- a/docs/migration/rownd/sdk-integration-guide.mdx +++ b/docs/migration/rownd/sdk-integration-guide.mdx @@ -1,20 +1,14 @@ --- title: SDK Integration Guide -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 3 description: Configure the SuperTokens Rownd backend plugin and frontend SDKs. -page_type: tutorial -category: migration +sidebar: + order: 2 --- -# SDK integration guide +Configure the SuperTokens Rownd backend plugin and frontend SDKs to migrate users, create SuperTokens sessions, and keep using Rownd-style APIs. - - Configure the SuperTokens Rownd backend plugin and frontend SDKs to migrate users, create SuperTokens sessions, and keep using Rownd-style APIs. - +--- - ## Overview @@ -27,11 +21,11 @@ These instructions assume that you have already created an account in [the Super After you have done that, select the relevant **Managed** deployment, enable **Account Linking** from **Features**, and copy the core connection information from **Overview**. -:::info no-title +:::info The Rownd compatibility plugin is only available with NodeJS or Python at the moment. If your main backend uses another language or unsupported framework, deploy the NodeJS or Python backend as an authentication sidecar and route Rownd/SuperTokens auth traffic to it. -Read [the complete guide](/docs/references/backend-sdks/other-frameworks) for more information on how to set it up. +Read [the complete guide](/references/backend-sdks/other-frameworks) for more information on how to set it up. ::: @@ -40,57 +34,67 @@ Read [the complete guide](/docs/references/backend-sdks/other-frameworks) for mo ### 1. Configure the backend SDK - - - - + + #### 1.1 Install the SuperTokens SDK and Rownd plugin Install the base SuperTokens backend SDK together with the Rownd migration plugin. The SuperTokens SDK adds the auth middleware, recipe APIs, and session handling. The Rownd plugin adds the Rownd-compatible migration, Hub, profile, and OAuth compatibility routes. + + +#### 1.1 Install the SuperTokens SDK and Rownd plugin - - - +Install the base SuperTokens Python SDK together with the Rownd migration plugin from [PyPI](https://pypi.org/project/supertokens-rownd/). + + + + + + ```bash npm install supertokens-node @supertokens-plugins/rownd-nodejs ``` - - - - - + + ```bash yarn add supertokens-node @supertokens-plugins/rownd-nodejs ``` - - - - - + + ```bash pnpm add supertokens-node @supertokens-plugins/rownd-nodejs ``` - - - - - + + ```bash bun add supertokens-node @supertokens-plugins/rownd-nodejs ``` + + + + + + +```bash +pip install supertokens-python supertokens-rownd +``` + + +```bash +uv add supertokens-python supertokens-rownd +``` + + + + - - - - + + #### 1.2 Initialize SuperTokens Initialize the recipes that map to your Rownd auth methods, then add the Rownd plugin under `experimental.plugins`. -:::info - -Contact the SuperTokens team before finalizing this setup for a complete plugin `appConfig` object based on your existing Rownd configuration. +:::info[Contact the SuperTokens team before finalizing this setup for a complete plugin `appConfig` object based on your existing Rownd configuration.] ::: @@ -100,7 +104,29 @@ The setup has four parts: - `appInfo`: defines the public API and website domains used by SuperTokens and the Rownd Hub. - `recipeList`: enables the SuperTokens recipes used to replace Rownd auth behavior. - `experimental.plugins`: mounts the Rownd migration plugin routes under `apiBasePath`. + + +#### 1.2 Initialize SuperTokens + +Python plugin configuration must include `api_base_path`, `api_domain`, `website_domain`, and `app_name` explicitly. Keep these values in sync with `InputAppInfo`. + +:::info + +Contact the SuperTokens team before finalizing this setup for a complete plugin `app_config` object based on your existing Rownd configuration. + +::: + +The setup has four parts: + +- `supertokens_config`: connects the backend SDK to SuperTokens Core. +- `app_info`: defines the public API and website domains used by SuperTokens and the Rownd Hub. +- `recipe_list`: enables the SuperTokens recipes used to replace Rownd auth behavior. +- `experimental.plugins`: mounts the Rownd migration plugin routes under `api_base_path`. + + + + ```ts import SuperTokens from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; @@ -217,110 +243,8 @@ SuperTokens.init({ }, }); ``` - -#### 1.3 Add CORS and middleware - -Install SuperTokens middleware after CORS handling. - -:::important - -- Add the `middleware` BEFORE all your routes. -- Add the `cors` middleware BEFORE the SuperTokens middleware as shown below. - -::: - -```ts -import express from "express"; -import cors from "cors"; -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/express"; - -const app = express(); - -app.use( - cors({ - origin: process.env.WEBSITE_DOMAIN, - allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - credentials: true, - }), -); - -// IMPORTANT: CORS should be before this line. -app.use(middleware()); - -// ...your API routes -``` - -#### 1.4 Configure client domains - -By default, magic links are constructed using the `websiteDomain` that you pass in the SDK configuration. -To test locally or to route the user to a mobile deep linked domain you can use the `clientDomains` plugin option. - -```ts -RowndMigrationPlugin.init({ - rowndAppKey: process.env.ROWND_APP_KEY!, - rowndAppSecret: process.env.ROWND_APP_SECRET!, - clientDomains: { - browser: "https://app.example.com", - browser_local: "http://localhost:3000", - mobile: "https://my-app.rownd-hub.supertokens.com", - }, -}); -``` - -The client sends a `clientDomain` key, not a URL. The plugin looks up the key in `clientDomains` and rewrites links to that base URL. - -If no explicit key is sent: - -- Mobile Hub flows use `clientDomains.mobile`. -- Browser Hub flows use `clientDomains.browser`. -- If the selected key is missing, the plugin keeps the link on the Hub URL and only rewrites the path. - - - - - -#### 1.1 Install the SuperTokens SDK and Rownd plugin - -Install the base SuperTokens Python SDK together with the Rownd migration plugin from [PyPI](https://pypi.org/project/supertokens-rownd/). - - - - - -```bash -pip install supertokens-python supertokens-rownd -``` - - - - - -```bash -uv add supertokens-python supertokens-rownd -``` - - - - - -#### 1.2 Initialize SuperTokens - -Python plugin configuration must include `api_base_path`, `api_domain`, `website_domain`, and `app_name` explicitly. Keep these values in sync with `InputAppInfo`. - -:::info no-title - -Contact the SuperTokens team before finalizing this setup for a complete plugin `app_config` object based on your existing Rownd configuration. - -::: - -The setup has four parts: - -- `supertokens_config`: connects the backend SDK to SuperTokens Core. -- `app_info`: defines the public API and website domains used by SuperTokens and the Rownd Hub. -- `recipe_list`: enables the SuperTokens recipes used to replace Rownd auth behavior. -- `experimental.plugins`: mounts the Rownd migration plugin routes under `api_base_path`. - + + ```python from supertokens_python import ( InputAppInfo, @@ -443,11 +367,53 @@ init( ), ) ``` + + + + + +#### 1.3 Add CORS and middleware + +Install SuperTokens middleware after CORS handling. + +:::note[- Add the `middleware` BEFORE all your routes.] +- Add the `cors` middleware BEFORE the SuperTokens middleware as shown below. + +::: + + #### 1.3 Add CORS and middleware For FastAPI, use the `get_middleware()` and `get_all_cors_headers()` functions as shown below. + + + + + +```ts +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/express"; + +const app = express(); + +app.use( + cors({ + origin: process.env.WEBSITE_DOMAIN, + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }), +); + +// IMPORTANT: CORS should be before this line. +app.use(middleware()); +// ...your API routes +``` + + ```python from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware @@ -472,13 +438,40 @@ app.add_middleware( # TODO: start server ``` + + + + +#### 1.4 Configure client domains + +By default, magic links are constructed using the `websiteDomain` that you pass in the SDK configuration. +To test locally or to route the user to a mobile deep linked domain you can use the `clientDomains` plugin option. + + #### 1.4 Configure client domains By default, magic links are constructed using the `website_domain` that you pass in the SDK configuration. To test locally or to route the user to a mobile deep linked domain you can use the `client_domains` plugin option. + + -```python + + +```ts check=false reason="Requires earlier Rownd setup context" +RowndMigrationPlugin.init({ + rowndAppKey: process.env.ROWND_APP_KEY!, + rowndAppSecret: process.env.ROWND_APP_SECRET!, + clientDomains: { + browser: "https://app.example.com", + browser_local: "http://localhost:3000", + mobile: "https://my-app.rownd-hub.supertokens.com", + }, +}); +``` + + +```python check=false reason="Requires earlier Rownd setup context" RowndPluginConfig( rownd_app_key="", rownd_app_secret="", @@ -489,7 +482,20 @@ RowndPluginConfig( }, ) ``` + + + + + +The client sends a `clientDomain` key, not a URL. The plugin looks up the key in `clientDomains` and rewrites links to that base URL. + +If no explicit key is sent: +- Mobile Hub flows use `clientDomains.mobile`. +- Browser Hub flows use `clientDomains.browser`. +- If the selected key is missing, the plugin keeps the link on the Hub URL and only rewrites the path. + + The client sends a `clientDomain` key, not a URL. The plugin looks up the key in `client_domains` and rewrites links to that base URL. If no explicit key is sent: @@ -497,12 +503,10 @@ If no explicit key is sent: - Mobile Hub flows use `client_domains["mobile"]`. - Browser Hub flows use `client_domains["browser"]`. - If the selected key is missing, the plugin keeps the link on the Hub URL and only rewrites the path. + + - - - - -#### 1.5 Configure Apple login for iOS {{optional}} +#### 1.5 Configure Apple login for iOS (optional) If your iOS app uses native Sign in with Apple, configure Apple as multiple SuperTokens clients. Browser and Hub Apple login use the Apple Services ID, but native iOS Apple login returns authorization codes for your app bundle ID. @@ -510,11 +514,9 @@ The iOS bundle ID must therefore be configured as a separate Apple client in the The Rownd plugin maps the Apple sign-in method to those SuperTokens client types. The plugin setting is `iosClientType`, which becomes `ios_client_type` in the Rownd app config. The iOS SDK reads that value and sends it as `clientType` when it exchanges the Apple authorization code with `/signinup`. - - - - -```ts + + +```ts check=false reason="Requires earlier Rownd setup context" ThirdParty.init({ signInAndUpFeature: { providers: [ @@ -557,12 +559,9 @@ RowndMigrationPlugin.init({ }, }); ``` - - - - - -```python + + +```python check=false reason="Requires earlier Rownd setup context" thirdparty.init( sign_in_and_up_feature=thirdparty.SignInAndUpFeature( providers=[ @@ -605,10 +604,8 @@ RowndPluginConfig( }, ) ``` - - - - + + If Android uses a separate Apple client, add another SuperTokens Apple client with `clientType: "android"` and set `androidClientType: "android"` on the Rownd Apple sign-in method. @@ -623,118 +620,61 @@ Every client needs the same values configured on the backend: - `apiBasePath`: the SuperTokens API base path, for example ``. - `clientDomain`: optional key from the backend `clientDomains` map. - + + +#### 2.1 Install the React SDK + + +#### 2.1 Load the hosted Hub script - +Use this option when you do not use a package-based frontend framework. + + +#### 2.1 Add the Android SDK -#### 2.1 Install the React SDK +The Android SDK is published through JitPack. + + +#### 2.1 Add the iOS SDK - +In Xcode, add this Swift Package dependency: + + +#### 2.1 Install the Flutter SDK - +Add the SuperTokens Rownd Flutter package to `pubspec.yaml`: + + +#### 2.1 Install the React Native SDK + + + + + + ```bash npm install @supertokens/rownd-react ``` - - - - - + + ```bash yarn add @supertokens/rownd-react ``` - - - - - + + ```bash pnpm add @supertokens/rownd-react ``` - - - - - + + ```bash bun add @supertokens/rownd-react ``` - - - - - -#### 2.2 Add the provider - -Replace imports from `@rownd/react` with `@supertokens/rownd-react`, then add `RowndProvider` near the root of your application. - -```tsx -import React from "react"; -import ReactDOM from "react-dom/client"; -import { RowndProvider } from "@supertokens/rownd-react"; -import { App } from "./App"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - ", - apiBasePath: "", - }, - }} - > - - , -); -``` - -Do not manually include the Hub script in your HTML when using the React SDK. The provider injects the Hub script for you. - -#### 2.3 Use Rownd-compatible APIs - -```tsx -import { RequireSignIn, SignedIn, SignedOut, useRownd } from "@supertokens/rownd-react"; - -export function AuthControls() { - const { requestSignIn, signOut, user } = useRownd(); - - return ( -
- - - - - - - - - -

{user.data?.email || user.data?.phone_number || user.id}

- -
- - -

Protected content

-
-
- ); -} -``` - -`requestSignIn()` supports Rownd-style options such as `identifier`, `auto_sign_in`, `init_data`, `post_login_redirect`, `include_user_data`, `redirect`, `intent`, `group_to_join`, `prevent_closing`, `method`, and `method_options`. - - - - - -#### 2.1 Load the hosted Hub script - -Use this option when you do not use a package-based frontend framework. - +
+
+
+ ```html ``` + + +```gradle +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url "https://jitpack.io" } + } +} +``` + + +```text +https://github.com/supertokens/supertokens-rownd-ios.git +``` + + +```yaml +dependencies: + supertokens_rownd_flutter: ^0.1.0 + provider: ^6.1.2 +``` + + + + +```bash +npm install @supertokens/rownd-react-native +``` + + +```bash +yarn add @supertokens/rownd-react-native +``` + + +```bash +pnpm add @supertokens/rownd-react-native +``` + + +```bash +bun add @supertokens/rownd-react-native +``` + + + +
+ + + +#### 2.2 Add the provider +Replace imports from `@rownd/react` with `@supertokens/rownd-react`, then add `RowndProvider` near the root of your application. + + The script URL supports these query parameters: | Parameter | Required | Description | @@ -765,7 +762,52 @@ The script URL supports these query parameters: #### 2.2 Use runtime config Use `window._rphConfig` for optional settings that are easier to set in JavaScript than in the script URL. + + +Select the `Rownd` package product and add it to your app target. +If you use CocoaPods instead, install the `RowndSupertokens` pod. The pod exposes the same Swift module, so app code still imports `Rownd`. + +#### 2.2 Configure Rownd + + +Then fetch dependencies: + + +React Native apps must use React Native `0.61` or newer. Native builds also need Android `minSdkVersion` `26` or newer and iOS deployment target `14.0` or newer. + +#### 2.2 Expo setup + +For Expo apps, add the plugin, a URL scheme, and native platform versions to `app.json`. Use a development build or prebuild so native URL scheme and platform configuration is generated. + + + + + +```tsx check=false reason="Requires earlier Rownd setup context" +import React from "react"; +import ReactDOM from "react-dom/client"; +import { RowndProvider } from "@supertokens/rownd-react"; +import { App } from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render( + ", + apiBasePath: "", + }, + }} + > + + , +); +``` + + ```html ``` - -Add `_rphConfig` entries before the Hub script loads. - - - - - -#### 2.1 Add the Android SDK - -The Android SDK is published through JitPack. - -```gradle -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - maven { url "https://jitpack.io" } - } -} -``` - + + ```gradle dependencies { implementation 'com.github.supertokens:supertokens-rownd-android:0.1.1' } ``` - -The SDK requires `compileSdk 35` or newer, Kotlin Gradle plugin `2.1.0` or newer, and `minSdk 26` or newer. - -#### 2.2 Add configuration values - -```gradle -android { - defaultConfig { - manifestPlaceholders = [rowndDeepLinkScheme: "rowndsupertokens"] - - buildConfigField "String", "ROWND_APP_KEY", '""' - buildConfigField "String", "ROWND_API_DOMAIN", '""' - buildConfigField "String", "ROWND_API_BASE_PATH", '""' - buildConfigField "String", "ROWND_DEEP_LINK_SCHEME", '"rowndsupertokens"' - } -} -``` - -#### 2.3 Configure deep links - -:::important - -Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain, including Android App Links metadata. - -::: - -Add one custom-scheme fallback filter and one verified HTTPS App Link filter. - -```xml - - - - - - - - - - - - - -``` - -The HTTPS App Link domain should match `clientDomains.mobile` on the backend. - -#### 2.4 Initialize Rownd - -```kotlin -import android.app.Application -import io.rownd.android.Rownd -import io.rownd.android.RowndConfigureOptions - -class MyApplication : Application() { - override fun onCreate() { - super.onCreate() - - Rownd.configure( - this, - RowndConfigureOptions( - appKey = BuildConfig.ROWND_APP_KEY, - apiDomain = BuildConfig.ROWND_API_DOMAIN, - apiBasePath = BuildConfig.ROWND_API_BASE_PATH, - deepLinkScheme = BuildConfig.ROWND_DEEP_LINK_SCHEME, - ) - ) - } -} -``` - -#### 2.5 Call protected APIs - -Rownd manages the SuperTokens session after sign-in. For `OkHttp`, add the SuperTokens interceptor to clients that call protected backend APIs. - -```kotlin -import com.supertokens.session.SuperTokensInterceptor -import okhttp3.OkHttpClient - -val client = OkHttpClient.Builder() - .addInterceptor(SuperTokensInterceptor()) - .build() -``` - - - - - -#### 2.1 Add the iOS SDK - -In Xcode, add this Swift Package dependency: - -```text -https://github.com/supertokens/supertokens-rownd-ios.git -``` - -Select the `Rownd` package product and add it to your app target. - -If you use CocoaPods instead, install the `RowndSupertokens` pod. The pod exposes the same Swift module, so app code still imports `Rownd`. - -#### 2.2 Configure Rownd - + + ```swift import Rownd import UIKit @@ -925,85 +848,124 @@ func application( return true } ``` + + +```bash +flutter pub get +``` + + +```json +{ + "expo": { + "scheme": "rowndsupertokens", + "plugins": [ + "@supertokens/rownd-react-native", + [ + "expo-build-properties", + { + "android": { + "minSdkVersion": 26 + }, + "ios": { + "deploymentTarget": "14.0" + } + } + ] + ] + } +} +``` + + -#### 2.3 Configure links + + +Do not manually include the Hub script in your HTML when using the React SDK. The provider injects the Hub script for you. -:::important +#### 2.3 Use Rownd-compatible APIs + + +Add `_rphConfig` entries before the Hub script loads. + + +The SDK requires `compileSdk 35` or newer, Kotlin Gradle plugin `2.1.0` or newer, and `minSdk 26` or newer. -Contact the SuperTokens team before configuring production Universal Links. We need to set up the link asset files for your Hub domain, including the Apple App Site Association file. +#### 2.2 Add configuration values + + +#### 2.3 Configure links + +:::note[Contact the SuperTokens team before configuring production Universal Links. We need to set up the link asset files for your Hub domain, including the Apple App Site Association file.] ::: Add an Associated Domains entitlement for the Hub domain used by the app. + + +#### 2.2 Configure Rownd -```xml -com.apple.developer.associated-domains - - applinks:my-app.rownd-hub.supertokens.com - -``` - -Register the custom URL scheme fallback. +Import the Flutter package and configure it before using any Rownd APIs. + + +Install the Expo build properties plugin before running prebuild: + + -```xml -CFBundleURLTypes - - - CFBundleURLSchemes - - rowndsupertokens - - - -``` + + +```tsx +import { RequireSignIn, SignedIn, SignedOut, useRownd } from "@supertokens/rownd-react"; -Forward custom URL scheme links and Universal Links to Rownd. +export function AuthControls() { + const { requestSignIn, signOut, user } = useRownd(); -```swift -func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - return Rownd.handleSmartLink(url: url) -} + return ( +
+ + + + + + + -func application( - _ application: UIApplication, - continue userActivity: NSUserActivity, - restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void -) -> Bool { - guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let url = userActivity.webpageURL else { - return false - } + +

{user.data?.email || user.data?.phone_number || user.data?.user_id}

+ +
- return Rownd.handleSmartLink(url: url) + +

Protected content

+
+
+ ); } ``` +
+ +```gradle +android { + defaultConfig { + manifestPlaceholders = [rowndDeepLinkScheme: "rowndsupertokens"] -The Universal Link domain should match `clientDomains.mobile` on the backend. - - - - - -#### 2.1 Install the Flutter SDK - -Add the SuperTokens Rownd Flutter package to `pubspec.yaml`: - -```yaml -dependencies: - supertokens_rownd_flutter: ^0.1.0 - provider: ^6.1.2 + buildConfigField "String", "ROWND_APP_KEY", '""' + buildConfigField "String", "ROWND_API_DOMAIN", '""' + buildConfigField "String", "ROWND_API_BASE_PATH", '""' + buildConfigField "String", "ROWND_DEEP_LINK_SCHEME", '"rowndsupertokens"' + } +} ``` - -Then fetch dependencies: - -```bash -flutter pub get + + +```xml +com.apple.developer.associated-domains + + applinks:my-app.rownd-hub.supertokens.com + ``` - -#### 2.2 Configure Rownd - -Import the Flutter package and configure it before using any Rownd APIs. - + + ```dart import 'package:supertokens_rownd_flutter/rownd.dart'; import 'package:supertokens_rownd_flutter/rownd_platform_interface.dart'; @@ -1023,13 +985,74 @@ void configureRownd() { )); } ``` + + +```bash +npx expo install expo-build-properties +``` + +
+ + + +`requestSignIn()` supports Rownd-style options such as `identifier`, `auto_sign_in`, `init_data`, `post_login_redirect`, `include_user_data`, `redirect`, `intent`, `group_to_join`, `prevent_closing`, `method`, and `method_options`. + + +#### 2.3 Configure deep links + +:::note[Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain, including Android App Links metadata.] + +::: +Add one custom-scheme fallback filter and one verified HTTPS App Link filter. + + +Register the custom URL scheme fallback. + + The Flutter package is published as `supertokens_rownd_flutter`. Existing Rownd-style APIs remain available through `RowndPlugin`, but the package import and SuperTokens config are required for the migrated SDK. #### 2.3 Use Rownd-compatible APIs The SDK exposes Rownd state through a `ChangeNotifier`. Provide `rowndPlugin.state()` to your widget tree and use the plugin methods for sign-in, sign-out, account management, user profile calls, and access tokens. + + +#### 2.3 Add the provider + + + + + +```xml + + + + + + + + + + + + +``` + + +```xml +CFBundleURLTypes + + + CFBundleURLSchemes + + rowndsupertokens + + + +``` + + ```dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -1099,150 +1122,9 @@ class AuthControls extends StatelessWidget { } } ``` - -`requestSignIn()` accepts an optional `RowndSignInOptions` object. The migrated Flutter SDK currently exposes `postSignInRedirect` as the sign-in option. - -#### 2.4 Configure Android - -Flutter Android apps need JitPack because the native SuperTokens Rownd Android SDK is resolved from JitPack. - -```gradle -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - maven { url "https://jitpack.io" } - } -} -``` - -Set Android platform versions and Kotlin metadata support: - -```gradle -android { - compileSdk 35 - - defaultConfig { - minSdk 26 - targetSdk 35 - } -} -``` - -Use Kotlin Gradle plugin `2.1.0` or newer. Also make your main activity extend `FlutterFragmentActivity` instead of `FlutterActivity`: - -```kotlin -import io.flutter.embedding.android.FlutterFragmentActivity - -class MainActivity : FlutterFragmentActivity() -``` - -#### 2.5 Configure iOS - -The Flutter plugin depends on the `RowndSupertokens` CocoaPod. The pod exposes the Swift module as `Rownd`, so Flutter apps do not need app-level Swift import changes. - -Install pods after adding the package: - -```bash -cd ios && pod install -``` - -If an existing lockfile pins an older `lottie-ios` version, update pods: - -```bash -cd ios && pod update lottie-ios --repo-update -``` - -#### 2.6 Configure mobile links - -:::important - -Contact the SuperTokens team before configuring production deep links or Universal Links. We need to set up the link asset files for your Hub domain for the native platforms your Flutter app supports. - -::: - -Configure the same native link handling described in the Android and iOS tabs for Flutter's Android and iOS host apps. The HTTPS App Link or Universal Link domain should match `clientDomains.mobile` on the backend. - - - - - -#### 2.1 Install the React Native SDK - - - - - -```bash -npm install @supertokens/rownd-react-native -``` - - - - - -```bash -yarn add @supertokens/rownd-react-native -``` - - - - - -```bash -pnpm add @supertokens/rownd-react-native -``` - - - - - -```bash -bun add @supertokens/rownd-react-native -``` - - - - - -React Native apps must use React Native `0.61` or newer. Native builds also need Android `minSdkVersion` `26` or newer and iOS deployment target `14.0` or newer. - -#### 2.2 Expo setup - -For Expo apps, add the plugin, a URL scheme, and native platform versions to `app.json`. Use a development build or prebuild so native URL scheme and platform configuration is generated. - -```json -{ - "expo": { - "scheme": "rowndsupertokens", - "plugins": [ - "@supertokens/rownd-react-native", - [ - "expo-build-properties", - { - "android": { - "minSdkVersion": 26 - }, - "ios": { - "deploymentTarget": "14.0" - } - } - ] - ] - } -} -``` - -Install the Expo build properties plugin before running prebuild: - -```bash -npx expo install expo-build-properties -``` - -#### 2.3 Add the provider - -```tsx + + +```tsx check=false reason="Requires earlier Rownd setup context" import { RowndProvider } from "@supertokens/rownd-react-native"; export default function Root() { @@ -1265,25 +1147,142 @@ export default function Root() { ); } ``` + + + + + +The HTTPS App Link domain should match `clientDomains.mobile` on the backend. + +#### 2.4 Initialize Rownd + + +Forward custom URL scheme links and Universal Links to Rownd. + + +`requestSignIn()` accepts an optional `RowndSignInOptions` object. The migrated Flutter SDK currently exposes `postSignInRedirect` as the sign-in option. + +#### 2.4 Configure Android +Flutter Android apps need JitPack because the native SuperTokens Rownd Android SDK is resolved from JitPack. + + The React Native provider accepts `appKey`, `supertokens.appInfo`, `deepLinkScheme`, and optional `hubUrlOverride`. Use `hubUrlOverride` only for staging or local Hub testing. React Native does not send a `clientDomain` prop; mobile Hub flows use the backend `clientDomains.mobile` default. #### 2.4 Register native links -:::important - -Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain for the native platforms your React Native app supports. +:::note[Contact the SuperTokens team before configuring production deep links. We need to set up the link asset files for your Hub domain for the native platforms your React Native app supports.] ::: For bare React Native iOS apps, install pods after adding the package: + + + + + +```kotlin +import android.app.Application +import io.rownd.android.Rownd +import io.rownd.android.RowndConfigureOptions + +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + Rownd.configure( + this, + RowndConfigureOptions( + appKey = BuildConfig.ROWND_APP_KEY, + apiDomain = BuildConfig.ROWND_API_DOMAIN, + apiBasePath = BuildConfig.ROWND_API_BASE_PATH, + deepLinkScheme = BuildConfig.ROWND_DEEP_LINK_SCHEME, + ) + ) + } +} +``` + + +```swift +func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + return Rownd.handleSmartLink(url: url) +} + +func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void +) -> Bool { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL else { + return false + } + return Rownd.handleSmartLink(url: url) +} +``` + + +```gradle +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url "https://jitpack.io" } + } +} +``` + + ```bash cd ios && pod install ``` + + + + + +#### 2.5 Call protected APIs +Rownd manages the SuperTokens session after sign-in. For `OkHttp`, add the SuperTokens interceptor to clients that call protected backend APIs. + + +The Universal Link domain should match `clientDomains.mobile` on the backend. + + +Set Android platform versions and Kotlin metadata support: + + Register the same scheme in `Info.plist` and forward URL opens to React Native `Linking`. The React Native Rownd provider listens for `Linking` events and passes matching links to the native SDK. + + + + + +```kotlin +import com.supertokens.session.SuperTokensInterceptor +import okhttp3.OkHttpClient + +val client = OkHttpClient.Builder() + .addInterceptor(SuperTokensInterceptor()) + .build() +``` + + +```gradle +android { + compileSdk 35 + defaultConfig { + minSdk 26 + targetSdk 35 + } +} +``` + + ```xml CFBundleURLTypes @@ -1295,9 +1294,27 @@ Register the same scheme in `Info.plist` and forward URL opens to React Native ` ``` + + + + +Use Kotlin Gradle plugin `2.1.0` or newer. Also make your main activity extend `FlutterFragmentActivity` instead of `FlutterActivity`: + + Objective-C app delegate: + + + + + +```kotlin +import io.flutter.embedding.android.FlutterFragmentActivity +class MainActivity : FlutterFragmentActivity() +``` + + ```objc #import @@ -1308,9 +1325,29 @@ Objective-C app delegate: return [RCTLinkingManager application:application openURL:url options:options]; } ``` + + + + + +#### 2.5 Configure iOS + +The Flutter plugin depends on the `RowndSupertokens` CocoaPod. The pod exposes the Swift module as `Rownd`, so Flutter apps do not need app-level Swift import changes. +Install pods after adding the package: + + Swift app delegate: + + + + +```bash +cd ios && pod install +``` + + ```swift import React @@ -1322,9 +1359,25 @@ override func application( return RCTLinkingManager.application(app, open: url, options: options) } ``` + + + + +If an existing lockfile pins an older `lottie-ios` version, update pods: + + For Android, register the scheme on the activity that hosts React Native and use `singleTask`. The scheme must match `config.deepLinkScheme`; `singleTask` is required so links opened while the app is running are delivered to the existing React Native activity. + + + + +```bash +cd ios && pod update lottie-ios --repo-update +``` + + ```xml ``` + + + + +#### 2.6 Configure mobile links + +:::note[Contact the SuperTokens team before configuring production deep links or Universal Links. We need to set up the link asset files for your Hub domain for the native platforms your Flutter app supports.] + +::: + +Configure the same native link handling described in the Android and iOS tabs for Flutter's Android and iOS host apps. The HTTPS App Link or Universal Link domain should match `clientDomains.mobile` on the backend. + + If your bare React Native Android app uses Google Sign-In, initialize the Rownd package from `MainActivity` before calling auth APIs: + + + + ```kotlin import android.os.Bundle import com.facebook.react.ReactActivity @@ -1353,9 +1423,17 @@ class MainActivity : ReactActivity() { } } ``` + + + + #### 2.5 Use Rownd-compatible APIs + + + + ```tsx import { Pressable, Text, View } from "react-native"; import { useRownd } from "@supertokens/rownd-react-native"; @@ -1389,12 +1467,14 @@ export function AuthControls() { ); } ``` + + + + `requestSignIn()` accepts Rownd-style options such as `method`, `postSignInRedirect`, and `intent`. The `guest` method is treated as `anonymous`. On Android, forcing `email` or `phone` currently opens the Hub default flow rather than bypassing the method selector. - - - -
+ + ### 3. Validate client flows @@ -1405,7 +1485,7 @@ Test the following flows to validate your client integration: - Existing Rownd sessions migrate without forcing users to sign in again. - Deep links work as expected on mobile -### 4. Migrate OAuth/OIDC clients {{optional}} +### 4. Migrate OAuth/OIDC clients (optional) If your Rownd application acts as an OAuth/OIDC provider, update clients to use SuperTokens discovery and endpoints after the SuperTokens team migrates your Rownd OAuth clients into SuperTokens Core. diff --git a/docs/migration/session-migration.mdx b/docs/migration/session-migration.mdx index bd248ed6ac..3ea4d38d64 100644 --- a/docs/migration/session-migration.mdx +++ b/docs/migration/session-migration.mdx @@ -1,24 +1,14 @@ --- -id: session-migration title: Session Migration -hide_title: true -sidebar_position: 3 -pagination_next: null -description: >- - Migrate user sessions from your previous authentication provider to - SuperTokens using backend and frontend changes. -page_type: tutorial -category: migration +description: Migrate user sessions from your previous authentication provider to SuperTokens using backend and frontend changes. +sidebar: + order: 3 --- +This guide shows you how to migrate user sessions from your previous authentication provider to **SuperTokens**. -# Session Migration - - - This guide will show you how to migrate your user sessions from you previous authentication provider to **SuperTokens**. - +--- - ## Overview @@ -27,64 +17,171 @@ To do this you should create a new flow that will determine if an existing user You can see a detailed illustration of the process below. -Session migration flow chart +Session migration flow chart ## Steps ### 1. Add the session migration endpoint -Create a new endpoint on your backend that will generate a new **SuperTokens Session** based on the current authentication token. -This will be called by the frontend if a user is still logged in through your previous authentication provider. +Create a rate-limited backend endpoint that exchanges a valid legacy access token for a **SuperTokens Session**. The +following example uses the APIs released in SuperTokens Node SDK 24.0.3. -```tsx title="Backend changes" +```tsx title="Backend changes" check=false reason="Requires surrounding framework application context" import express from "express"; +import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; -let app = express(); - -app.post("/migrate-session", async (req, res) => { - - // extract the access token from the request object - if(req.headers.authorization !== undefined){ - let access_token = req.headers.authorization.split("Bearer ")[1]; +const app = express(); +app.use(express.json()); - // verify the access token and retrieve the old userId - let customUserId = await verifyAccessTokenAndRetriveUserId(access_token); +interface VerifiedLegacyToken { + issuer: string; + subject: string; + tenantId: string; +} - // create a new SuperTokens session using the customUserId - // the createNewSession function will attach the SuperTokens session tokens to the response object. - // @ts-ignore - await Session.createNewSession(req, res, customUserId) - res.send({ - status: "OK" - }) +app.post("/migrate-session", migrationRateLimiter, async (req, res, next) => { + const match = req.headers.authorization?.match(/^Bearer[ \t]+([^\s,]+)$/i); + const idempotencyKey = req.header("Idempotency-Key"); + if (match === null || match === undefined || !isValidIdempotencyKey(idempotencyKey)) { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; + } + + let verifiedToken: VerifiedLegacyToken; + try { + verifiedToken = await verifyLegacyAccessToken(match[1]); + } catch { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; + } + + try { + const identity = getNamespacedLegacyIdentity(verifiedToken); + await enforceVerifiedIdentityRateLimit(identity.identityKey); + + const mapping = await SuperTokens.getUserIdMapping({ + userId: identity.externalUserId, + userIdType: "EXTERNAL", + }); + if (mapping.status !== "OK") { + res.status(401).send({ status: "INVALID_LEGACY_TOKEN" }); + return; } - // handle access_token not present in request - -}) - -async function verifyAccessTokenAndRetriveUserId(access_token: string): Promise { - // verify the access_token and return the decoded userId - return "..."; + const recipeUserId = SuperTokens.convertToRecipeUserId(mapping.superTokensUserId); + const result = await migrateSessionIdempotently( + { + idempotencyKey, + identityKey: identity.identityKey, + tenantId: verifiedToken.tenantId, + recipeUserId: recipeUserId.getAsString(), + }, + () => Session.createNewSession(req, res, verifiedToken.tenantId, recipeUserId), + ); + if (result.status === "CONFLICT") { + res.status(409).send({ status: "MIGRATION_CONFLICT" }); + return; + } + if (result.status === "IN_PROGRESS") { + res.set("Retry-After", "1").status(409).send({ status: "MIGRATION_IN_PROGRESS" }); + return; + } + res.send({ status: result.status }); + } catch (error) { + next(error); + } +}); + +app.post("/confirm-session-migration", migrationRateLimiter, async (req, res, next) => { + const idempotencyKey = req.body?.idempotencyKey; + if (!isValidIdempotencyKey(idempotencyKey)) { + res.status(400).send({ status: "INVALID_IDEMPOTENCY_KEY" }); + return; + } + + try { + const session = await Session.getSession(req, res); + const confirmed = await confirmMigrationOutcome({ + idempotencyKey, + tenantId: session.getTenantId(), + recipeUserId: session.getRecipeUserId().getAsString(), + }); + res.status(confirmed ? 200 : 409).send({ status: confirmed ? "CONFIRMED" : "IDENTITY_MISMATCH" }); + } catch (error) { + next(error); + } +}); + +function isValidIdempotencyKey(value: unknown): value is string { + return typeof value === "string" && /^[A-Za-z0-9_-]{32,128}$/.test(value); } +declare function migrationRateLimiter(req: express.Request, res: express.Response, next: express.NextFunction): void; +declare function enforceVerifiedIdentityRateLimit(identityKey: string): Promise; + +declare function getNamespacedLegacyIdentity(verifiedToken: VerifiedLegacyToken): { + identityKey: string; + externalUserId: string; +}; + +declare function migrateSessionIdempotently( + input: { + idempotencyKey: string; + identityKey: string; + tenantId: string; + recipeUserId: string; + }, + createSession: () => ReturnType, +): Promise<{ status: "CREATED" | "RECOVERED" | "CONFLICT" | "IN_PROGRESS" }>; + +declare function confirmMigrationOutcome(input: { + idempotencyKey: string; + tenantId: string; + recipeUserId: string; +}): Promise; + +// Implement this contract with your provider's supported SDK or a JWT library configured for that provider. +declare function verifyLegacyAccessToken(accessToken: string): Promise; ``` -:::info Important +:::info[Important] -Verifying the access token will require you to access your previous providers JWKS endpoint. -This means that you will have to continue to use your previous auth provider even after switching to SuperTokens. +`verifyLegacyAccessToken` is an application-owned security boundary, not a provider-independent implementation. It must +verify the signature with the provider's trusted algorithms and keys, and validate the exact issuer, audience, expiry, +not-before time, token type or purpose, and required subject claims. If the provider uses JWKS, use HTTPS, cache keys, +and refresh them safely when keys rotate. Derive `tenantId` from trusted application configuration and validated claims, +never directly from request data that has not been verified. -If you want to immediately stop querying your previous authentication provider you can use the JWKS public keys and provide them as a secret to the JWT verification function. -You can follow our [guide](/docs/additional-verification/session-verification/protect-api-routes#with-the-public-key-string) on how to do this. +`getNamespacedLegacyIdentity` must use the same collision-resistant canonical encoding used during account import. Its +external ID and identity key must include the validated issuer, trusted tenant, and subject. Do not concatenate ambiguous +strings or map by subject alone: subjects are only unique within an issuer and can also overlap between tenants. ::: +Configure CORS on the backend with the exact frontend origin, `credentials: true`, and the `Authorization`, +`Content-Type`, and `Idempotency-Key` request headers explicitly allowed. Do not combine credentialed requests with +`Access-Control-Allow-Origin: *`. The endpoint's error handler should return a generic 401 for verification failures +without exposing token-validation details. + +Implement `migrateSessionIdempotently` with a distributed outcome store indexed uniquely by both the stable request key +and the legacy identity scoped to its issuer and tenant. A completed outcome records the tenant, recipe user ID, and +created session handle. A +retry for the same key and identity must return or safely recover that logical outcome; if the original session was not +delivered, revoke it before issuing a replacement. Reject a key bound to another identity, another key for an already +migrated identity, and concurrent in-progress exchanges. Never store the raw legacy token. Keep completed outcomes at +least until the legacy session and token can no longer be accepted. `confirmMigrationOutcome` must compare the current +SuperTokens tenant and recipe user ID with that stored outcome and atomically mark it confirmed. + ### 2. Call the migration endpoint from your frontend app -On your frontend, on page load, check if a session with your previous authentication provider exists. -- If a session exists, send a request to the `/migrate-session` API with the access token to create a new SuperTokens session. -- Revoke the old session. +On page load, obtain the legacy token and its stable request key. If a SuperTokens session exists, confirm that it +matches the stored migration outcome before cleaning up the legacy session. Otherwise, perform the idempotent exchange, +then confirm the identities. Never treat an HTTP success alone as proof that the current SuperTokens and legacy +identities match. The example uses SuperTokens Web JS 0.16.0 and assumes that the SDK is initialized. ```tsx title="Frontend changes" import axios from "axios"; @@ -93,40 +190,71 @@ import Session from "supertokens-web-js/recipe/session"; // Call this function on page load async function migrateUserSessions() { - let apiDomain = "..."; - // On page load retrieve the users access token if a session exists - let accessToken = await getAccessTokenFromOldProvider() - - if (accessToken !== undefined) { - - // send a request to your migrate session endpoint with the bearer token - await axios.post(`${apiDomain}/migrate-session`, { - headers: { - "Authorization": `Bearer ${accessToken}` - } - }) - await revokeSessionFromOldProvider() + const apiDomain = "..."; + const accessToken = await getAccessTokenFromOldProvider(); + if (accessToken === undefined) { + return; + } + + const idempotencyKey = await getOrCreateMigrationIdempotencyKey(); + + if (!(await Session.doesSessionExist())) { + await axios.post( + `${apiDomain}/migrate-session`, + {}, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Idempotency-Key": idempotencyKey, + }, + withCredentials: true, + }, + ); + } + + if (!(await confirmMigratedIdentity(apiDomain, idempotencyKey))) { + return; + } + + await revokeSessionFromOldProvider(); + await clearMigrationIdempotencyKey(); +} + +async function confirmMigratedIdentity(apiDomain: string, idempotencyKey: string): Promise { + try { + const response = await axios.post( + `${apiDomain}/confirm-session-migration`, + { idempotencyKey }, + { withCredentials: true }, + ); + return response.data.status === "CONFIRMED"; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 409) { + return false; } + throw error; + } } async function getAccessTokenFromOldProvider(): Promise { - // Check if a session with your your previous provider exists and return the access_token. Return undefined otherwise - return "..." + // Return the provider's access token when its session exists, or undefined otherwise. + return "..."; } +// Persist one random key for this specific legacy provider session until cleanup succeeds. +declare function getOrCreateMigrationIdempotencyKey(): Promise; +declare function clearMigrationIdempotencyKey(): Promise; + async function revokeSessionFromOldProvider() { - // Revoke the session associated with the previous provider + // Revoke the session associated with the previous provider } - ``` --- ## See also - - - - - - - + + + + + diff --git a/docs/platform-configuration/_category_.json b/docs/platform-configuration/_category_.json deleted file mode 100644 index 3fb78a0e60..0000000000 --- a/docs/platform-configuration/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Platform Configuration", - "collapsible": true, - "collapsed": true, - "customProps": { - "isMainCategory": true - }, - "position": 7 -} diff --git a/docs/platform-configuration/email-delivery.mdx b/docs/platform-configuration/email-delivery.mdx index 00e13d9dcc..7d992a0f35 100644 --- a/docs/platform-configuration/email-delivery.mdx +++ b/docs/platform-configuration/email-delivery.mdx @@ -1,19 +1,27 @@ --- -title: Email delivery -hide_title: true -sidebar_position: 3 -description: Customize email delivery methods. -page_type: guide -category: platform-configuration +title: Configure Email Delivery +description: Send SuperTokens emails through the default service, your own SMTP server and domain, or a custom delivery implementation. +sidebar: + order: 3 --- + -# Email delivery +## Email delivery summary + +- Email delivery is owned by the `EmailPassword`, `EmailVerification`, `Passwordless`, and `WebAuthn` recipes. `AccountLinking` does not configure delivery. +- Without configuration, each recipe uses its built-in delivery service. The endpoint and failure behavior differ by recipe and SDK; this service does not support template customization. +- Configure your own SMTP server to send from your domain and optionally customize the subject and template. +- For complete control, provide a custom delivery implementation or override `sendEmail`. + + ## Overview SuperTokens sends emails in different authentication scenarios. -The method applies in the `EmailPassword`, `Passwordless`, and `AccountLinking` recipes. +Email delivery is configured on the recipe that generates the message: `EmailPassword` for password resets, +`EmailVerification`, `Passwordless` for email codes and links, and `WebAuthn` for account-recovery emails. +The `AccountLinking` recipe does not own an email-delivery configuration. The following page shows you how to configure the email delivery method and adjust the content that gets sent to your users. @@ -21,82 +29,95 @@ The following page shows you how to configure the email delivery method and adju ### Default service -If you provide no configuration for email delivery, the backend SDK sends emails by talking to the servers on `https://api.supertokens.com` -This applies to both self hosted and managed services. +If you provide no email-delivery configuration, the recipe uses the backend SDK's built-in delivery service. This applies +whether the Core is self-hosted or managed. Do not allow the external delivery endpoints from a single hostname: released +recipes use both `api.supertokens.io` and `api.supertokens.com`. + +:::note +The built-in service does not support template customization. Configure SMTP or a custom delivery implementation when +you need to control the sender, content, delivery guarantees, or data-processing terms. +::: -:::important -- No info sent to the API that sends out emails on behalf of your app is logged or stored. -- The system sends emails using `noreply@supertokens.io` email ID. If you want to use your own domain, please see one of the other methods in this section. -- You cannot customize the email template when using this method. If you want to customize the emails, please see one of the other methods in this section. -- Emails sent via the service are free and may land up in user's spam due to the high number of apps that use the service. If you want to avoid this, we recommend using one of the other methods mentioned in this section. +:::caution[Failure behavior differs] +Do not treat the built-in service as a durable queue. In Node.js 24.0.3, Passwordless awaits delivery, while several other +email flows suppress built-in-service failures outside serverless environments. If delivery is security-critical, provide +your own service, await its result, monitor failures, and make retries idempotent. ::: --- ### SMTP service -Using this method, you can provide your own SMTP server's configuration and the system sends the emails using those. +Using this method, you can provide your own SMTP server configuration and the system sends emails through it. Use this method if you want to: - Send emails using your own domain. - Optionally customize the default email template and subject. - - - + + ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import { SMTPService } from "supertokens-node/recipe/emailpassword/emaildelivery"; -import EmailVerification from "supertokens-node/recipe/emailverification" +import EmailVerification from "supertokens-node/recipe/emailverification"; import { SMTPService as EmailVerificationSMTPService } from "supertokens-node/recipe/emailverification/emaildelivery"; - -// highlight-start -let smtpSettings = { - host: "...", - authUsername: "...", // this is optional. In case not given, from.email will be used - password: "...", - port: 465, - from: { - name: "...", - email: "...", - }, - secure: true -} -// highlight-end +import Passwordless from "supertokens-node/recipe/passwordless"; +import { SMTPService as PasswordlessSMTPService } from "supertokens-node/recipe/passwordless/emaildelivery"; +import WebAuthn from "supertokens-node/recipe/webauthn"; +import { SMTPService as WebAuthnSMTPService } from "supertokens-node/recipe/webauthn/emaildelivery"; + +const smtpSettings = { + host: "...", + authUsername: "...", // this is optional. In case not given, from.email will be used + password: "...", + port: 465, + from: { + name: "...", + email: "...", + }, + secure: true, +}; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - emailDelivery: { - service: new SMTPService({smtpSettings}) - }, - // highlight-end - }), - - // if email verification is enabled.. - EmailVerification.init({ - mode: "OPTIONAL", - // highlight-start - emailDelivery: { - service: new EmailVerificationSMTPService({smtpSettings}) - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + service: new SMTPService({ smtpSettings }), + }, + }), + + // if email verification is enabled.. + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + service: new EmailVerificationSMTPService({ smtpSettings }), + }, + }), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + emailDelivery: { + service: new PasswordlessSMTPService({ smtpSettings }), + }, + }), + WebAuthn.init({ + emailDelivery: { + service: new WebAuthnSMTPService({ smtpSettings }), + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" @@ -104,13 +125,13 @@ import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword/epmodels" "github.com/supertokens/supertokens-golang/recipe/emailverification" "github.com/supertokens/supertokens-golang/recipe/emailverification/evmodels" + "github.com/supertokens/supertokens-golang/recipe/passwordless" + "github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels" "github.com/supertokens/supertokens-golang/supertokens" - "crypto/tls" ) func main() { - // highlight-start smtpUsername := "..." smtpSettings := emaildelivery.SMTPSettings{ Host: "...", @@ -121,52 +142,51 @@ func main() { Port: 465, Username: &smtpUsername, // this is optional. In case not given, from.email will be used Password: "...", - Secure: false, + Secure: true, - // this is optional. TLS config is used if Secure is set to true, or server supports STARTTLS - // if not provided, the SDK will use a default config - TLSConfig: &tls.Config{ - // ... - }, } - // highlight-end supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Service: emailpassword.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: smtpSettings, }), }, - // highlight-end }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Service: emailverification.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: smtpSettings, }), }, - // highlight-end + }), + passwordless.Init(plessmodels.TypeInput{ + ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", + EmailDelivery: &emaildelivery.TypeInput{ + Service: passwordless.MakeSMTPService(emaildelivery.SMTPServiceConfig{ + Settings: smtpSettings, + }), + }, }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig, SMTPSettingsFrom, SMTPSettings from supertokens_python.recipe import emailverification +from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig -# highlight-start smtp_settings = SMTPSettings( host="...", port=465, @@ -175,110 +195,112 @@ smtp_settings = SMTPSettings( email="..." ), password="...", - secure=False, + secure=True, username="..." # this is optional. In case not given, from_.email will be used ) -# highlight-end init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( - # highlight-start email_delivery=EmailDeliveryConfig( service=emailpassword.SMTPService( smtp_settings=smtp_settings ) ) - # highlight-end ), # If email verification is enabled emailverification.init( mode="OPTIONAL", - # highlight-start email_delivery=EmailDeliveryConfig( service=emailverification.SMTPService( smtp_settings=smtp_settings ) ) - # highlight-end + ), + passwordless.init( + contact_config=ContactEmailOnlyConfig(), + flow_type="USER_INPUT_CODE", + email_delivery=EmailDeliveryConfig( + service=passwordless.SMTPService(smtp_settings=smtp_settings) + ) ) ] ) ``` + + - - +Port 465 conventionally uses implicit TLS, so the examples set `secure`/`Secure` to `true`. For STARTTLS, use the port +specified by your provider (commonly 587) and set `secure`/`Secure` to `false`; the connection starts without encryption +and is then upgraded. Never disable certificate or hostname verification. Node.js 24.0.3 exports a WebAuthn SMTP template +service. Python 0.31.3 and Go 0.26.0 accept WebAuthn email-delivery implementations but do not export a public, +recipe-specific WebAuthn SMTP template service; configure a WebAuthn delivery override in those SDKs instead of importing +an internal module or reusing another recipe's template service. ### Custom method This method allows you to define your own email sending abstraction. - - - + + ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; -import EmailVerification from "supertokens-node/recipe/emailverification" +import EmailVerification from "supertokens-node/recipe/emailverification"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - // TODO: create and send password reset email - - // Or use the original implementation which calls the default service, - // or a service that you may have specified in the emailDelivery object. - return originalImplementation.sendEmail(input); - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + // TODO: create and send password reset email + + // Or use the original implementation which calls the default service, + // or a service that you may have specified in the emailDelivery object. + return originalImplementation.sendEmail(input); }, - // highlight-end - }), - - // if email verification is enabled - EmailVerification.init({ - mode: "OPTIONAL", - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - // TODO: create and send email verification email - - // Or use the original implementation which calls the default service, - // or a service that you may have specified in the emailDelivery object. - return originalImplementation.sendEmail(input); - } - } - } + }; + }, + }, + }), + + // if email verification is enabled + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + // TODO: create and send email verification email + + // Or use the original implementation which calls the default service, + // or a service that you may have specified in the emailDelivery object. + return originalImplementation.sendEmail(input); }, - // highlight-end - }), - Session.init() - ] + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" @@ -293,7 +315,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail @@ -309,13 +330,11 @@ func main() { return originalImplementation }, }, - // highlight-end }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail @@ -331,16 +350,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword @@ -358,7 +375,7 @@ def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> # Or use the original implementation which calls the default service, # or a service that you may have specified in the email_delivery object. return await original_send_email(template_vars, user_context) - + original_implementation.send_email = send_email return original_implementation @@ -378,31 +395,30 @@ def custom_emailverification_delivery(original_implementation: EVEmailDeliveryOv init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_emailverification_delivery)) ] ) ``` - - - + + If you call the original implementation function for `sendEmail`, it uses the service that you have configured. If you have not configured any service, it uses the default service. Using this method, you can, for example, have your custom way of sending email verification emails, but use the default or SMTP service to send the reset password emails. -:::important Error Management -To handle failures and other types of issues in the `sendEmail` function throw an error. The SDK catches any exception through an upper level error handler. This error is then propagated if this is through an API call or otherwise logged. +:::note[Error management] +Throw or return an error from your custom `sendEmail` implementation when delivery fails. API-triggered delivery can +propagate the error through the SDK's error handler; non-API calls may log it. This does not describe every recipe's +built-in service behavior; see the warning under [Default service](#default-service). ::: --- @@ -410,8 +426,8 @@ To handle failures and other types of issues in the `sendEmail` function throw a ## Email content customization You can access the default email UI through the following links: -- Default [email verification template](/docs/references/frontend-sdks/prebuilt-ui/ui-showcase#email-verification) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html). -- Default [password reset template](/docs/references/frontend-sdks/prebuilt-ui/ui-showcase#password-reset) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). +- Default [email verification template](/references/frontend-sdks/prebuilt-ui/ui-showcase#email-verification) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/email-verification.html). +- Default [password reset template](/references/frontend-sdks/prebuilt-ui/ui-showcase#password-reset) and its [source code](https://github.com/supertokens/email-sms-templates/blob/master/email-html/password-reset.html). To change the content you can create a custom `SMTPService` like and update the property which builds the content. The method allows you to return an object that has the following properties: @@ -420,85 +436,81 @@ The method allows you to return an object that has the following properties: - `subject`: This is the subject of the email to send. - `toEmail`: The system sends the email to this email. -Other information like which email ID to send from appears in the `smtpSettings` object. - +Other information like which email address to send from appears in the `smtpSettings` object. - - -```tsx + + +```tsx check=false reason="Requires surrounding application context" import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; import { SMTPService } from "supertokens-node/recipe/emailpassword/emaildelivery"; -import EmailVerification from "supertokens-node/recipe/emailverification" +import EmailVerification from "supertokens-node/recipe/emailverification"; import { SMTPService as EmailVerificationSMTPService } from "supertokens-node/recipe/emailverification/emaildelivery"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - emailDelivery: { - service: new SMTPService({ - // @ts-ignore - smtpSettings: { /*...*/ }, - // highlight-start - override: (originalImplementation) => { - return { - ...originalImplementation, - getContent: async function (input) { - // password reset content - let { passwordResetLink, user } = input; - - // you can even call the original implementation and modify that - let originalContent = await originalImplementation.getContent(input) - originalContent.subject = "My custom subject"; - return originalContent; - } - } - } - // highlight-end - }) - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + service: new SMTPService({ + smtpSettings: { + /*...*/ + }, + override: (originalImplementation) => { + return { + ...originalImplementation, + getContent: async function (input) { + // password reset content + let { passwordResetLink, user } = input; + + // you can even call the original implementation and modify that + let originalContent = await originalImplementation.getContent(input); + originalContent.subject = "My custom subject"; + return originalContent; + }, + }; + }, }), - - // if email verification is enabled - EmailVerification.init({ - mode: "OPTIONAL", - emailDelivery: { - service: new EmailVerificationSMTPService({ - // @ts-ignore - smtpSettings: { /*...*/ }, - // highlight-start - override: (originalImplementation) => { - return { - ...originalImplementation, - getContent: async function (input) { - // email verification content - let { emailVerifyLink, user } = input; - - // you can even call the original implementation and modify that - let originalContent = await originalImplementation.getContent(input) - originalContent.subject = "My custom subject"; - return originalContent; - } - } - } - // highlight-end - }) - } + }, + }), + + // if email verification is enabled + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + service: new EmailVerificationSMTPService({ + smtpSettings: { + /*...*/ + }, + override: (originalImplementation) => { + return { + ...originalImplementation, + getContent: async function (input) { + // email verification content + let { emailVerifyLink, user } = input; + + // you can even call the original implementation and modify that + let originalContent = await originalImplementation.getContent(input); + originalContent.subject = "My custom subject"; + return originalContent; + }, + }; + }, }), - Session.init() - ] + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "fmt" @@ -515,12 +527,10 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - // REMOVE_FROM_OUTPUT EmailDelivery: &emaildelivery.TypeInput{ Service: emailpassword.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: emaildelivery.SMTPSettings{ /* ... */ }, - // highlight-start Override: func(originalImplementation emaildelivery.SMTPInterface) emaildelivery.SMTPInterface { originalGetContent := *originalImplementation.GetContent @@ -542,7 +552,6 @@ func main() { return originalImplementation }, - // highlight-end }), }, }), @@ -553,7 +562,6 @@ func main() { Service: emailverification.MakeSMTPService(emaildelivery.SMTPServiceConfig{ Settings: emaildelivery.SMTPSettings{ /* ... */ }, - // highlight-start Override: func(originalImplementation emaildelivery.SMTPInterface) emaildelivery.SMTPInterface { originalGetContent := *originalImplementation.GetContent @@ -575,7 +583,6 @@ func main() { return originalImplementation }, - // highlight-end }), }, }), @@ -583,10 +590,9 @@ func main() { }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import emailpassword from supertokens_python.ingredients.emaildelivery.types import EmailDeliveryConfig, EmailContent, SMTPSettings @@ -598,7 +604,6 @@ from supertokens_python.recipe import emailverification def custom_smtp_content_override(original_implementation: SMTPOverrideInput) -> SMTPOverrideInput: original_get_content = original_implementation.get_content - # highlight-start async def get_content(template_vars: EmailTemplateVars, user_context: Dict[str, Any]) -> EmailContent: # password reset content _ = template_vars.password_reset_link @@ -608,7 +613,6 @@ def custom_smtp_content_override(original_implementation: SMTPOverrideInput) -> original_content = await original_get_content(template_vars, user_context) original_content.subject = "My custom subject" return original_content - # highlight-end original_implementation.get_content = get_content return original_implementation @@ -616,7 +620,6 @@ def custom_smtp_content_override(original_implementation: SMTPOverrideInput) -> def custom_smtp_email_verification_content_override(original_implementation: EVSMTPOverrideInput) -> EVSMTPOverrideInput: original_get_content = original_implementation.get_content - # highlight-start async def get_content(template_vars: EVEmailTemplateVars, user_context: Dict[str, Any]) -> EmailContent: # email verification content _ = template_vars.email_verify_link @@ -626,7 +629,6 @@ def custom_smtp_email_verification_content_override(original_implementation: EVS original_content = await original_get_content(template_vars, user_context) original_content.subject = "My custom subject" return original_content - # highlight-end original_implementation.get_content = get_content return original_implementation @@ -635,109 +637,98 @@ def custom_smtp_email_verification_content_override(original_implementation: EVS init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( - # highlight-start email_delivery=EmailDeliveryConfig( service=emailpassword.SMTPService( - smtp_settings=SMTPSettings(...), # type: ignore + smtp_settings=SMTPSettings(...), override=custom_smtp_content_override ) ) - # highlight-end ), # If email verification is enabled emailverification.init( mode="OPTIONAL", - # highlight-start email_delivery=EmailDeliveryConfig( service=emailverification.SMTPService( - smtp_settings=SMTPSettings(...), # type: ignore + smtp_settings=SMTPSettings(...), override=custom_smtp_email_verification_content_override ) ) - # highlight-end ) ] ) ``` - - - + + ## Overrides You can use the override functionality to trigger any kind of behavior before and after email sending. This can include things like: -- Logging +- Logging - Spam protection actions - Modifying the email template variables before sending the emails - - - + + ```tsx import supertokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; -import EmailVerification from "supertokens-node/recipe/emailverification" +import EmailVerification from "supertokens-node/recipe/emailverification"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - // TODO: run some logic before sending the email - - await originalImplementation.sendEmail(input); - - // TODO: run some logic post sending the email - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + // TODO: run some logic before sending the email + + await originalImplementation.sendEmail(input); + + // TODO: run some logic post sending the email }, - // highlight-end - }), - - // if email verification is enabled - EmailVerification.init({ - mode: "OPTIONAL", - // highlight-start - emailDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendEmail: async function (input) { - // TODO: run some logic before sending the email - - await originalImplementation.sendEmail(input); - - // TODO: run some logic post sending the email - } - } - } + }; + }, + }, + }), + + // if email verification is enabled + EmailVerification.init({ + mode: "OPTIONAL", + emailDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendEmail: async function (input) { + // TODO: run some logic before sending the email + + await originalImplementation.sendEmail(input); + + // TODO: run some logic post sending the email }, - // highlight-end - }), - Session.init() - ] + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/emaildelivery" @@ -752,7 +743,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ emailpassword.Init(&epmodels.TypeInput{ - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail @@ -772,13 +762,11 @@ func main() { return originalImplementation }, }, - // highlight-end }), // if email verification is enabled emailverification.Init(evmodels.TypeInput{ Mode: evmodels.ModeRequired, - // highlight-start EmailDelivery: &emaildelivery.TypeInput{ Override: func(originalImplementation emaildelivery.EmailDeliveryInterface) emaildelivery.EmailDeliveryInterface { originalSendEmail := *originalImplementation.SendEmail @@ -798,16 +786,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.emailpassword.types import EmailDeliveryOverrideInput, EmailTemplateVars from supertokens_python.recipe import emailpassword @@ -826,7 +812,7 @@ def custom_email_deliver(original_implementation: EmailDeliveryOverrideInput) -> # TODO: run some logic after sending the email return resp - + original_implementation.send_email = send_email return original_implementation @@ -849,21 +835,18 @@ def custom_emailverification_delivery(original_implementation: EVEmailDeliveryOv init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ emailpassword.init( - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_email_deliver) ), # If email verification is enabled emailverification.init( mode="OPTIONAL", - # highlight-next-line email_delivery=EmailDeliveryConfig(override=custom_emailverification_delivery)) ] ) ``` - - - + + diff --git a/docs/platform-configuration/meta.ts b/docs/platform-configuration/meta.ts new file mode 100644 index 0000000000..64a9cf28cc --- /dev/null +++ b/docs/platform-configuration/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Platform Configuration", + icon: "settings", + order: 80, + collapsed: true, +}); diff --git a/docs/platform-configuration/sms-delivery.mdx b/docs/platform-configuration/sms-delivery.mdx index aad2f8c7fa..e9a6eb86c9 100644 --- a/docs/platform-configuration/sms-delivery.mdx +++ b/docs/platform-configuration/sms-delivery.mdx @@ -1,19 +1,15 @@ --- title: SMS delivery -hide_title: true -sidebar_position: 5 description: Customize SMS delivery process. -page_type: guide -category: platform-configuration +sidebar: + order: 5 --- - -# SMS Delivery - ## Overview SuperTokens sends SMS in different authentication scenarios. -The method applies to the `Passwordless` and `MFA` recipes. +SMS delivery is configured by the `Passwordless` recipe. Phone OTP can be used as an MFA factor, but the `MFA` recipe +does not expose a separate SMS-delivery configuration. The following page shows you how to configure the SMS delivery method and adjust the content that gets sent to your users. @@ -21,22 +17,28 @@ The following page shows you how to configure the SMS delivery method and adjust ### Default method -If you provide no configuration for SMS delivery, the backend SDK sends SMSs by talking to SuperTokens servers on `https://api.supertokens.com`. -This applies to both self hosted and managed services. +If you provide no configuration for SMS delivery, the Passwordless recipe uses the backend SDK's built-in service at +`https://api.supertokens.com/0/services/sms`. This applies whether the Core is self-hosted or managed. -:::info Important -- This is a free service and is globally rate limited. This is not suitable for production use. If you do not receive an SMS using this method, then the SDK prints the SMS content on the terminal. -- We do not log / store any of the info sent to the API that sends out emails on behalf of your app. +:::info[Important] +- Do not depend on the built-in service for production delivery. Its quota and availability are service policy, not an SDK contract. +- When the service returns HTTP 429, released SDK fallback implementations treat that response as terminal and print the message input. This can include the phone number, OTP, magic link, and code lifetime. - You cannot customize the SMS content when using this method. If you want to customize the content, please see one of the other methods in this section. ::: -### Twilio +:::caution[Sensitive fallback logs] +OTP codes and magic links are authentication secrets. Prevent production fallback logs from reaching shared consoles or +third-party log pipelines. If you must retain them for testing, restrict access, redact the phone number and secret values, +set a short retention period, and verify deletion. Prefer configuring Twilio or a custom service before production so a +quota response cannot expose message content through this fallback. +::: -Using this method, you can provide your own Twilio account details to the backend SDK, and the SMS is sent using those. +### Twilio - - +Using this method, you can provide your own Twilio account details to the backend SDK, and the SMS is sent using those. + + ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; @@ -44,40 +46,36 @@ import Session from "supertokens-node/recipe/session"; import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - // highlight-start - smsDelivery: { - service: new TwilioService({ - twilioSettings: { - accountSid: "...", - authToken: "...", - opts: { - // optionally extra config to pass to Twilio client - }, - - // give either from or messagingServiceSid - from: "...", - messagingServiceSid: "...", - }, - }) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + accountSid: "...", + authToken: "...", + opts: { + // optionally extra config to pass to Twilio client }, - // highlight-end + + // Use exactly one sender option. This example uses from. + from: "...", + }, }), - Session.init() - ] + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" @@ -88,53 +86,49 @@ import ( func main() { - // highlight-start smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{ Settings: smsdelivery.TwilioSettings{ AccountSid: "...", AuthToken: "...", - // Pass only one of From or MessagingServiceSid - From: "...", - MessagingServiceSid: "...", + // Use exactly one sender option. This example uses From. + From: "...", }, }) if err != nil { panic(err) } - // highlight-end supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start + ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: smsService, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, TwilioSettings init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT - # highlight-start + contact_config=ContactPhoneOnlyConfig(), + flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.TwilioService( twilio_settings=TwilioSettings( @@ -144,82 +138,57 @@ init( # Optional configs to pass to twilio client }, - # give either from_ or messaging_service_sid + # Use exactly one sender option. This example uses from_. from_="...", - messaging_service_sid="...", ) ) ) - # highlight-end ) ] ) ``` - - - + + To learn about how to customize the SMS templates, please see the next section. -### SuperTokens SMS Service +### SuperTokens SMS service -Using this method, SuperTokens sends messages to your users automatically. -Use this method if: -- You are already using the managed service. This is the quickest way to set up SMS sending. -- You want to take advantage of some SMS sending optimizations we do over time: - - Reduce SMS cost by picking a local SMS service based on the destination country. - - Fight spam / misuse of SMS. +The backend SDKs also expose an API-key-based `SuperTokensSMSService`. It calls an external SMS endpoint directly and can +be used whether your Core is self-hosted or managed. Availability, pricing, credits, quotas, sender identity, key issuance, +and the Dashboard workflow are mutable service policy. Confirm them in your current Dashboard or contract before adopting +this option; they are not guaranteed by the released SDK interface. -:::important -This is a paid service and charges apply for every SMS based on the cost incurred. -New users receive $10 worth of credits. -::: - -#### 1. Get the SMS API key - - - ## Sign up on SuperTokens.com - ## Create a new development `env` - ## Scroll down and create a new production `env` (it takes a few minutes to create one) - ## Once the production `env` is set up, you can find your SMS API key under the setup section: - - -SMS API key screen - -#### 2. Set the SMS API key in the backend SDK configuration - - - +If you have been issued an SMS API key, set it in the backend SDK configuration: + + ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; -import { SupertokensService } from "supertokens-node/recipe/passwordless/smsdelivery" +import { SupertokensService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - // highlight-start - smsDelivery: { - service: new SupertokensService("") - }, - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + smsDelivery: { + service: new SupertokensService(""), + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" @@ -232,95 +201,90 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start + ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: passwordless.MakeSupertokensSMSService(""), }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT + contact_config=ContactPhoneOnlyConfig(), + flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.SuperTokensSMSService("")) ) ] ) ``` - - - + + ### Custom method This method allows you to send messages however you like. The input to the send function consists of SMS template variables, allowing you to create the content of the SMS as well. Use this method if you are: -- Using a third party SMS service that is **not** Twilio. +- Using a third-party SMS service that is **not** Twilio. - You want to use another delivery method like WhatsApp or Facebook Messenger. - You want to do some custom spam protection before sending the SMS. - You already have an SMS sending infrastructure and want to use that. - - - + + ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - // highlight-start - smsDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendSms: async function ({ - codeLifetime, // amount of time the code is alive for (in MS) - phoneNumber, - urlWithLinkCode, // magic link - userInputCode, // OTP - }) { - // TODO: create and send SMS - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + smsDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendSms: async function ({ + codeLifetime, // amount of time the code is alive for (in MS) + phoneNumber, + urlWithLinkCode, // magic link + userInputCode, // OTP + }) { + // TODO: create and send SMS }, - // highlight-end - }), - Session.init() - ] + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "fmt" @@ -335,7 +299,8 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start + ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface { @@ -360,25 +325,23 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from typing import Dict, Any from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput: - # highlight-start async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None: # amount of time the code is alive for (in MS) _ = template_vars.code_life_time @@ -387,35 +350,35 @@ def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMS ____ = template_vars.user_input_code # OTP # TODO: create and send SMS... - # highlight-end original_implementation.send_sms = send_sms return original_implementation init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT + contact_config=ContactPhoneOnlyConfig(), + flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver) ) ] ) ``` - - - + + If you call the original implementation function for `sendSms`, it uses the service that you have configured. If you have not configured any service, it uses the default service. -:::info Important +:::info[Important] When using this callback, you must manage sending the SMS yourself. ::: -:::important Error Management -To handle failures and other types of issues in the `sendSms` function throw an error. The SDK catches any exception through an upper level error handler. This error is then propagated if this is through an API call or otherwise logged. +:::note[Error Management] +Throw or return an error from `sendSms` when delivery fails. API-triggered delivery can propagate it through the SDK's +error handler; non-API calls may log it. Do not include phone numbers, OTPs, magic links, provider credentials, or complete +provider responses in exceptions or logs. ::: ## SMS Customization @@ -427,80 +390,76 @@ You can see the default SMS content: To change the content of the default SMS templates, you can override the `getContent` function in the `smsDelivery` object. It allows you to return an object that has the following properties: -- `body`: This is the email's body. This can be HTML or text as well. +- `body`: The SMS message body. - `toPhoneNumber`: The phone number where the SMS is sent to. - - - -```tsx + + +```tsx check=false reason="Requires surrounding application context" import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; import { TwilioService } from "supertokens-node/recipe/passwordless/smsdelivery"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - smsDelivery: { - service: new TwilioService({ - // @ts-ignore - twilioSettings: { /*...*/ }, - // highlight-start - override: (originalImplementation) => { - return { - ...originalImplementation, - getContent: async function ({ - isFirstFactor, - codeLifetime, // amount of time the code is alive for (in MS) - phoneNumber, - urlWithLinkCode, // magic link - userInputCode, // OTP - }) { - if (isFirstFactor) { - // send some custom SMS content - return { - toPhoneNumber: phoneNumber, - body: "SMS BODY" - } - } else { - // for second factor, urlWithLinkCode will always be - // undefined since we only support OTP based for second factor - return { - toPhoneNumber: phoneNumber, - body: "SMS BODY" - } - } - - // You can even call the original implementation and - // modify its content: - - /*let originalContent = await originalImplementation.getContent(input) + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + smsDelivery: { + service: new TwilioService({ + twilioSettings: { + /*...*/ + }, + override: (originalImplementation) => { + return { + ...originalImplementation, + getContent: async function ({ + isFirstFactor, + codeLifetime, // amount of time the code is alive for (in MS) + phoneNumber, + urlWithLinkCode, // magic link + userInputCode, // OTP + }) { + if (isFirstFactor) { + // send some custom SMS content + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY", + }; + } else { + // for second factor, urlWithLinkCode will always be + // undefined since we only support OTP based for second factor + return { + toPhoneNumber: phoneNumber, + body: "SMS BODY", + }; + } + + // You can even call the original implementation and + // modify its content: + + /*let originalContent = await originalImplementation.getContent(input) originalContent.body = "My custom body"; return originalContent;*/ - - } - } - } - // highlight-end - }) - } + }, + }; + }, }), - Session.init() - ] + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "fmt" @@ -512,7 +471,6 @@ import ( ) func main() { - // highlight-start smsService, err := passwordless.MakeTwilioService(smsdelivery.TwilioServiceConfig{ Settings: smsdelivery.TwilioSettings{ /* ... */ }, Override: func(originalImplementation smsdelivery.TwilioInterface) smsdelivery.TwilioInterface { @@ -533,15 +491,15 @@ func main() { fmt.Println(urlWithLinkCode) fmt.Println(userInputCode) - // send some custom email content + // send custom SMS content return smsdelivery.SMSContent{ Body: "SMS BODY", ToPhoneNumber: phoneNumber, }, nil - // Or you can call the original implemenation and change its content: + // Or call the original implementation and change its content: /* - originalResponse, err := originalGetContent(input, userContex) + originalResponse, err := originalGetContent(input, userContext) if err != nil { return smsdelivery.SMSContent{}, nil } @@ -556,27 +514,26 @@ func main() { if err != nil { panic(err) } - // highlight-end supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start + ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Service: smsService, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from supertokens_python.recipe.passwordless.types import TwilioOverrideInput, SMSTemplateVars from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig, SMSContent, TwilioSettings from typing import Dict, Any @@ -586,7 +543,6 @@ def custom_sms_content_override(original_implementation: TwilioOverrideInput) -> # original_get_content = original_implementation.get_content - # highlight-start async def get_content(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> SMSContent: # amount of time the code is alive for (in MS) _ = template_vars.code_life_time @@ -594,15 +550,14 @@ def custom_sms_content_override(original_implementation: TwilioOverrideInput) -> __ = template_vars.url_with_link_code # magic link ___ = template_vars.user_input_code # OTP - # send some custom email content - return SMSContent(body="EMAIL BODY", to_phone=phone_number) + # send custom SMS content + return SMSContent(body="SMS BODY", to_phone=phone_number) # you can even call the original implementation and modify that # original_content = await original_get_content(template_vars, user_context) # original_content.body = "My custom body" # return original_content - # highlight-end original_implementation.get_content = get_content return original_implementation @@ -611,80 +566,73 @@ def custom_sms_content_override(original_implementation: TwilioOverrideInput) -> init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT - # highlight-start + contact_config=ContactPhoneOnlyConfig(), + flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig( service=passwordless.TwilioService( - # type: ignore - twilio_settings=TwilioSettings(...), # type: ignore + + twilio_settings=TwilioSettings(...), override=custom_sms_content_override ) ) - # highlight-end ) ] ) ``` - - - + + ## Overrides You can use the override functionality to trigger any kind of behavior before and after SMS sending. This can include things like: -- Logging +- Logging - Spam protection actions -- Modifying the email template variables before sending the emails - +- Modifying the SMS template variables before sending messages - - + + ```tsx import supertokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; import Session from "supertokens-node/recipe/session"; supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE", - contactMethod: "PHONE", - // highlight-start - smsDelivery: { - override: (originalImplementation) => { - return { - ...originalImplementation, - sendSms: async function (input) { - // TODO: before sending SMS - - await originalImplementation.sendSms(input) - - // TODO: after sending SMS - } - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE", + contactMethod: "PHONE", + smsDelivery: { + override: (originalImplementation) => { + return { + ...originalImplementation, + sendSms: async function (input) { + // TODO: before sending SMS + + await originalImplementation.sendSms(input); + + // TODO: after sending SMS }, - // highlight-end - }), - Session.init() - ] + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/ingredients/smsdelivery" @@ -697,7 +645,8 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ passwordless.Init(plessmodels.TypeInput{ - // highlight-start + ContactMethodPhone: plessmodels.ContactMethodPhoneConfig{Enabled: true}, + FlowType: "USER_INPUT_CODE", SmsDelivery: &smsdelivery.TypeInput{ Override: func(originalImplementation smsdelivery.SmsDeliveryInterface) smsdelivery.SmsDeliveryInterface { @@ -718,19 +667,18 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe.passwordless.types import SMSDeliveryOverrideInput, SMSTemplateVars from supertokens_python.recipe import passwordless +from supertokens_python.recipe.passwordless import ContactPhoneOnlyConfig from typing import Dict, Any from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig @@ -738,14 +686,12 @@ from supertokens_python.ingredients.smsdelivery.types import SMSDeliveryConfig def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMSDeliveryOverrideInput: original_send_sms = original_implementation.send_sms - # highlight-start async def send_sms(template_vars: SMSTemplateVars, user_context: Dict[str, Any]) -> None: # TODO: before sending SMS await original_send_sms(template_vars, user_context) # TODO: after sending SMS - # highlight-end original_implementation.send_sms = send_sms return original_implementation @@ -754,16 +700,15 @@ def custom_sms_deliver(original_implementation: SMSDeliveryOverrideInput) -> SMS init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ passwordless.init( - contact_config="", # type: ignore # REMOVE_FROM_OUTPUT - flow_type="USER_INPUT_CODE", # REMOVE_FROM_OUTPUT + contact_config=ContactPhoneOnlyConfig(), + flow_type="USER_INPUT_CODE", sms_delivery=SMSDeliveryConfig(override=custom_sms_deliver) ) ] ) ``` - - - + + diff --git a/docs/platform-configuration/supertokens-core/_category_.json b/docs/platform-configuration/supertokens-core/_category_.json deleted file mode 100644 index c837c99d66..0000000000 --- a/docs/platform-configuration/supertokens-core/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "SuperTokens Core", - "position": 1 -} diff --git a/docs/platform-configuration/supertokens-core/add-ssl-via-nginx.mdx b/docs/platform-configuration/supertokens-core/add-ssl-via-nginx.mdx index 79ade640b1..8a47b116dc 100644 --- a/docs/platform-configuration/supertokens-core/add-ssl-via-nginx.mdx +++ b/docs/platform-configuration/supertokens-core/add-ssl-via-nginx.mdx @@ -1,121 +1,79 @@ --- title: Add SSL via NGINX -hide_title: true -sidebar_position: 5 -description: >- - Set up SSL for secure SuperTokens Core connections using NGINX as a reverse - proxy. -page_type: guide -category: platform-configuration +description: Set up SSL for secure SuperTokens Core connections using NGINX as a reverse proxy. +sidebar: + order: 5 --- -# Add SSL via nginx - ## Overview -This section guides you through setting up SSL via Nginx to query the SuperTokens Core with a secure connection. +SuperTokens Core does not terminate TLS. This guide configures NGINX as a trusted TLS edge and redirects HTTP traffic to +HTTPS. ## Before you start -:::caution no-title -This page is only relevant if you are self hosting SuperTokens. +:::warning +This page is only relevant if you are self-hosting SuperTokens. ::: -This guide assumes you have already installed Nginx on your server. - -## Steps - -The following example guide runs SuperTokens `localhost:3567` - -### 1. Reverse proxy the SuperTokens core with nginx - -The SuperTokens core does not support SSL, and Nginx is needed as a reverse proxy to set up a secure connection. - -Start by opening the default Nginx site configuration file in a code editor. This file resides at: - - Linux: `/etc/nginx/sites-available/default`. - - Mac: `/usr/local/etc/nginx/sites-available/default`. - - Windows: `C:\nginx\conf\nginx.conf`. - -In the configuration, scroll down to the `server` directive. - -- By default it should look like this: +This guide assumes NGINX is installed and Core listens on `127.0.0.1:3567`. - ```text title="/etc/nginx/sites-available/default" - server { - listen 80; - server_name localhost; - ... - } - ``` - -- Configure the `server` directive by adding the `location` directive with the following values: - - ```text title="/etc/nginx/sites-available/default" - server { - listen 80; - server_name localhost; - // highlight-start - location / { - proxy_pass http://localhost:3567; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } - // highlight-end - } - ``` - -The `location` directive tells Nginx what to do with the incoming request, `proxy_pass` points the redirect to `localhost:3567`. +:::danger +Bind Core's port 3567 to `127.0.0.1` or a private network and block it from public ingress. If clients can connect to Core +directly, they can bypass TLS and controls enforced by NGINX. Core is a trusted backend component and must never be +directly reachable by browsers or clients you do not trust. +::: -- Test and apply the changes to Nginx by running the following command: +## Steps - ```bash - nginx -t && service nginx restart - ``` +### 1. Obtain a certificate -We can use the `/hello` API of the SuperTokens core to test the connection. +For a local test only, create a self-signed certificate: -Navigate to `http://localhost/hello` and check if it gives a valid response from the core. +```bash +sudo install -d -m 700 /etc/nginx/ssl +sudo openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout /etc/nginx/ssl/server.key \ + -out /etc/nginx/ssl/server.crt \ + -subj "/CN=localhost" +``` -### 2. Set up SSL +Use a CA-issued certificate valid for the production hostname in production. -Obtain a digital certificate to enable a secure connection with a user's browser. +### 2. Configure NGINX -Self-signed certificates will be used since development is local. However, certificate authorities like [Let's Encrypt](https://letsencrypt.org/) can also generate valid certificates. +Add separate HTTP and HTTPS server blocks. Replace `localhost` and certificate paths with production values when needed. -- Run the following command to generate a self signed certificate using OpenSSL: +```text title="/etc/nginx/sites-available/default" +server { + listen 80; + server_name localhost; + return 301 https://$server_name$request_uri; +} - ```bash - openssl req -x509 -nodes -newkey rsa:2048 -keyout /etc/nginx/ssl/server.key -out /etc/nginx/ssl/server.crt - ``` +server { + listen 443 ssl; + server_name localhost; -- Set the values `ssl_certificate` and `ssl_certificate_key` in the Nginx configuration to specify the locations of the newly generated certificates. + ssl_certificate /etc/nginx/ssl/server.crt; + ssl_certificate_key /etc/nginx/ssl/server.key; - ```text title="/etc/nginx/sites-available/default" - server { - listen 80; - listen 443 ssl; - server_name localhost; - // highlight-start - ssl_certificate /etc/nginx/ssl/server.crt; - ssl_certificate_key /etc/nginx/ssl/server.key; - // highlight-end - location / { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - } + location / { + proxy_pass http://127.0.0.1:3567; + proxy_http_version 1.1; + proxy_set_header Host $host; } - ``` +} +``` -- Run the test and restart commands to test and apply your changes: +Test and apply the configuration: ```bash -nginx -t && service nginx restart +sudo nginx -t +sudo service nginx reload ``` +Verify that `http://localhost/hello` redirects to HTTPS and `https://localhost/hello` reaches Core. `/hello` is +unauthenticated and is only a basic process/storage signal; success does not prove API-key enforcement or that direct port +3567 is private. Test externally that port 3567 is unreachable, and test a protected Core API with no, wrong, and current +API keys. diff --git a/docs/platform-configuration/supertokens-core/api-keys.mdx b/docs/platform-configuration/supertokens-core/api-keys.mdx index 3f3b31bb81..32153f3db6 100644 --- a/docs/platform-configuration/supertokens-core/api-keys.mdx +++ b/docs/platform-configuration/supertokens-core/api-keys.mdx @@ -1,130 +1,183 @@ --- -title: Add API Keys -hide_title: true -sidebar_position: 1 -description: >- - Authenticate backend SDK requests to SuperTokens core by adding API keys to - configuration. -page_type: guide -category: platform-configuration +title: API Keys +description: Require API keys for backend SDK requests to a self-hosted Core and rotate multiple keys without downtime. +sidebar: + order: 1 --- + -# Add API keys +## API key summary + +- SuperTokens Core requires no API key by default. After you configure one, every backend SDK request must provide a matching key or Core returns HTTP 401. +- Configure multiple keys as a comma-separated value to rotate keys gradually across backend systems. +- Every key must be at least 20 characters and contain only alphanumeric characters, `=`, or `-`. + + ## Overview -The backend SDK uses API keys to authenticate requests to the SuperTokens core. +The backend SDK uses API keys to authenticate requests to SuperTokens Core. -By default, there is no API key required. If you add an API key to the core's configuration or use the managed service, you need to add it to your backend SDK code. Otherwise, the core throws a `401` error. +By default, there is no API key required. After you configure one, every backend SDK must send a matching key or Core responds with HTTP 401. -:::caution -Running without an API key is only safe when the Core is on a private network reachable exclusively by your backend. If the Core can be reached from anywhere else, configure an API key — and ideally also restrict access by [IP address](/docs/platform-configuration/supertokens-core/ip-allow-deny) and serve traffic over [TLS/SSL](/docs/platform-configuration/supertokens-core/add-ssl-via-nginx). Any caller that can reach the Core can perform administrative operations on your users' data. See [Secure the core](/docs/deployment/self-host-supertokens#secure-the-core). +:::danger +Core is a trusted backend component with APIs that can administer users and sessions. Keep Core on a private network that +is reachable only by your backend services. Never expose it directly to browsers or clients you do not trust. An API key +is defense in depth, not a replacement for network isolation. Use TLS if the key crosses a network you do not trust. + +Without an API key, any caller that can reach Core can perform administrative operations on your users' data. Configure +an API key, restrict access by [IP address](/platform-configuration/supertokens-core/ip-allow-deny), and serve traffic over +[TLS/SSL](/platform-configuration/supertokens-core/add-ssl-via-nginx). See [Secure the core](/deployment/self-host-supertokens#secure-the-core). ::: ## Before you start -:::caution no-title -This page is only relevant if you are self hosting SuperTokens. +:::warning +This page is only relevant if you are self-hosting SuperTokens. ::: ## Steps ### 1. Add the key to the core instance -You can set the API by updating the instance parameters. - - - +Generate a high-entropy key and store it in your deployment's secret manager. For example: ```bash - docker run \ - -p 3567:3567 \ - -e API_KEYS= \ - -d supertokens/supertokens- +openssl rand -hex 32 ``` - - +The command prints a 64-character key that satisfies Core's character restrictions. Store it as +`SUPERTOKENS_API_KEY` in your deployment's secret manager. Do not commit it to source control, logs, shell history, or an +image layer. Set `SUPERTOKENS_IMAGE` to an immutable, verified image reference rather than an untagged image or `latest`. + + +```bash +: "${SUPERTOKENS_IMAGE:?Set an immutable Core image reference}" +: "${SUPERTOKENS_API_KEY:?Load a generated Core API key from secret storage}" +if [[ ! "$SUPERTOKENS_API_KEY" =~ ^[A-Za-z0-9=-]{20,}(,[A-Za-z0-9=-]{20,})*$ ]]; then + echo "SUPERTOKENS_API_KEY must contain one or more valid comma-separated Core API keys" >&2 + exit 1 +fi + +docker run \ + --network app-network \ + -e API_KEYS="$SUPERTOKENS_API_KEY" \ + -d "$SUPERTOKENS_IMAGE" +``` + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command +# Replace this entire placeholder from secret storage before starting Core. +# Core rejects the literal placeholder because underscores are not valid API-key characters. -api_keys: +api_keys: "" ``` - - + + - The format of the value is `key1,key2,key3`. -- Keys can only contain `=`, `-` and alpha-numeric (including capital) chars. -- Each key must have a minimum length of 20 chars -- An example value is `"Akjnv3iunvsoi8=-sackjij3ncisds,asnj9=asdcda-OI982JIUN=-a"`. Notice the `,` in the string which separates the two keys `"Akjnv3iunvsoi8=-sackjij3ncisds"` and `"asnj9=asdcda-OI982JIUN=-a"`. In the backend SDK, you should only provide one of these keys. +- Keys can only contain `=`, `-`, and alphanumeric characters. +- Each key must have a minimum length of 20 characters. This is a syntax requirement, not an entropy recommendation. +- Each backend sends only one key. Core can accept multiple independently generated keys separated by commas. -:::info -The reason for having multiple API keys is that it allows for key rotation to occur gradually if you have multiple backend systems querying the core. -::: +Keep the Core and backend secret records separate, even if both workloads expose their value as +`SUPERTOKENS_API_KEY`. During rotation, the Core record contains `old-key,new-key`; each backend secret record contains +exactly one of those keys. ### 2. Add the key to your backend code -Update the backend SDK initialization code to include the API key. - - - +Inject one key currently accepted by Core into each backend as `SUPERTOKENS_API_KEY`. + + ```tsx import supertokens from "supertokens-node"; +const apiKey = process.env.SUPERTOKENS_API_KEY; +if (apiKey === undefined || apiKey.length === 0) { + throw new Error("SUPERTOKENS_API_KEY is required"); +} + supertokens.init({ - supertokens: { - connectionURI: "", - // highlight-next-line - apiKey: "" - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [] + supertokens: { + connectionURI: "", + apiKey, + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], }); ``` - - - + + ```go -import "github.com/supertokens/supertokens-golang/supertokens" +import ( + "os" + + "github.com/supertokens/supertokens-golang/supertokens" +) func main() { + apiKey := os.Getenv("SUPERTOKENS_API_KEY") + if apiKey == "" { + panic("SUPERTOKENS_API_KEY is required") + } supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "", - //highlight-next-line - APIKey: "", + APIKey: apiKey, }, }) } ``` - - + + +```python check=false reason="Partial configuration example" +import os -```python from supertokens_python import init, InputAppInfo, SupertokensConfig +api_key = os.environ["SUPERTOKENS_API_KEY"] +if not api_key: + raise RuntimeError("SUPERTOKENS_API_KEY is required") + init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( connection_uri='', - # highlight-next-line - api_key='' + api_key=api_key ), - framework='...', # type: ignore + framework='...', recipe_list=[ #... ] ) ``` - - - + + + +### 3. Rotate a key safely + +Use an overlap period so that Core never rejects a backend that has not been updated yet: + +1. Generate a new independent key and store it in secret storage. Keep the old key active. +2. Set Core's `API_KEYS` value to `old-key,new-key`, deploy or restart every Core instance, and explicitly test a protected + API with each key from a trusted network. Do not continue unless both work. +3. Rotate all backends to the new key. Use a staged deployment where possible. Monitor backend request failures, the HTTP + 401 rate in Core or edge telemetry, and deployment health throughout the change. Core does not identify which matching + key was used, so use controlled old-key and new-key probes to verify both paths during the overlap. +4. Confirm every backend is healthy on the new key and that no planned rollback still depends on the old key. Then remove + the old key from Core and deploy every Core instance. Verify the new key works and the old key now receives HTTP 401. +5. Retain the old key securely for a defined rollback window, but do not leave it active in Core. A rollback must first + re-add the old key to Core, verify both keys, and only then roll back a backend. Destroy the old key after the window. + +Never replace the old key in Core before all Core instances accept the new key, and never remove it while any backend +still uses it. diff --git a/docs/platform-configuration/supertokens-core/base-path.mdx b/docs/platform-configuration/supertokens-core/base-path.mdx index 0301c2a044..8bfcd2e417 100644 --- a/docs/platform-configuration/supertokens-core/base-path.mdx +++ b/docs/platform-configuration/supertokens-core/base-path.mdx @@ -1,127 +1,113 @@ --- -id: base-path title: Add a base path -hide_title: true -sidebar_position: 6 -description: >- - Configure a base path for self-hosted core APIs by updating core config and - backend SDK. -page_type: guide -category: platform-configuration +description: Configure a base path for self-hosted core APIs by updating core config and backend SDK. +sidebar: + order: 6 --- - -# Adding a base path - ## Overview -If you cannot add a dedicated (sub) domain for the core and want to expose it to an external network, you may need to add a base path to all the core APIs. +If you cannot add a dedicated subdomain for Core, you can add a base path to all Core APIs. To do this, you have to make changes to the core's configuration as well as to the backend SDK's `init` function call. +:::danger +A base path changes routing only. It does not authenticate requests, hide Core, or provide access control. Keep Core on a +private network reachable only by trusted backend services. If broader network reachability is unavoidable, also require +an API key, terminate TLS at a trusted edge, and restrict ingress with firewall or security-group rules. +::: + Consider an example where the core resides on `http://localhost:3567/some-prefix`. This implies that all APIs exposed by the core are on `http://localhost:3567/some-prefix/*`. ## Before you start -:::caution no-title -This page is only relevant if you are self hosting SuperTokens. +:::warning +This page is only relevant if you are self-hosting SuperTokens. ::: -The feature is only available for core versions `>= 3.9` +The feature is only available for Core versions `>= 3.9`. ## Steps -### 1. Change the core configuration - - - +### 1. Change the core configuration + + ```bash docker run \ - -p 3567:3567 \ - // highlight-next-line - -e BASE_PATH="/some-prefix" \ - -d supertokens/supertokens- + -p 127.0.0.1:3567:3567 \ + -e BASE_PATH="/some-prefix" \ + -d "$SUPERTOKENS_IMAGE" ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command base_path: "/some-prefix" ``` - - + + ### 2. Change the backend SDK initialization - - - + + ```tsx import supertokens from "supertokens-node"; supertokens.init({ - supertokens: { - // highlight-next-line - connectionURI: "http://localhost:3567/some-prefix", - // ... - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [/* ... */ ] + supertokens: { + connectionURI: "http://localhost:3567/some-prefix", + // ... + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + /* ... */ + ], }); ``` - - - + + ```go import "github.com/supertokens/supertokens-golang/supertokens" func main() { supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ - // highlight-next-line ConnectionURI: "http://localhost:3567/some-prefix", }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, SupertokensConfig init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), supertokens_config=SupertokensConfig( - # highlight-next-line connection_uri='http://localhost:3567/some-prefix', ), - framework='...', # type: ignore + framework='...', recipe_list=[ #... ] ) ``` + + - - - -:::note - -You can even set different base paths for different core instances: +:::note[You can even set different base paths for different core instances:] - For each of the core's configs you need to supply their base path as mentioned in step 1 -- The connection URI should be something like `"/;/;/;"`. For example, a valid connection URI can be `"http://localhost:3567/some-prefix;http://localhost:3567/some-prefix-2"` +- The connection URI should be something like `"/;/"`. For example, a valid connection URI is `"http://localhost:3567/some-prefix;http://localhost:3567/some-prefix-2"`. ::: - diff --git a/docs/platform-configuration/supertokens-core/cli.mdx b/docs/platform-configuration/supertokens-core/cli.mdx index ca6696e166..eb434a548c 100644 --- a/docs/platform-configuration/supertokens-core/cli.mdx +++ b/docs/platform-configuration/supertokens-core/cli.mdx @@ -1,17 +1,10 @@ --- title: CLI -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 10 -description: >- - Learn to use SuperTokens CLI for starting, listing, stopping, and uninstalling - instances. -page_type: guide -category: platform-configuration +description: Learn to use SuperTokens CLI for starting, listing, stopping, and uninstalling instances. +sidebar: + order: 10 --- -# SuperTokens CLI - ## Overview The SuperTokens CLI has the allows you to manage your core instance from the command line. @@ -20,8 +13,7 @@ The SuperTokens CLI has the allows you to manage your core instance from the com supertokens [command] [--help] [--version] ``` -:::important -If you are using Windows, you can only use the SuperTokens CLI using a terminal with Administrator privilege. +:::note[If you are using Windows, you can only use the SuperTokens CLI using a terminal with Administrator privilege.] ::: ## Commands diff --git a/docs/platform-configuration/supertokens-core/ip-allow-deny.mdx b/docs/platform-configuration/supertokens-core/ip-allow-deny.mdx index 1f3c0ca34c..e9c5beb97b 100644 --- a/docs/platform-configuration/supertokens-core/ip-allow-deny.mdx +++ b/docs/platform-configuration/supertokens-core/ip-allow-deny.mdx @@ -1,26 +1,25 @@ --- title: Filter requests based on IP address -hide_title: true -sidebar_position: 2 -description: >- - Configure SuperTokens to allow or deny requests based on specific IP addresses - for enhanced security. -page_type: guide -category: platform-configuration +description: Configure SuperTokens to allow or deny requests based on specific IP addresses for enhanced security. +sidebar: + order: 2 --- - -# Filter requests based on IP address - ## Overview -You can set the SuperTokens core's configuration such that it accepts / denies requests that originate from certain IPs. -This ensures that only your backend can query the SuperTokens core - increasing the security. +You can configure SuperTokens Core to allow or deny requests from specific directly connected peer addresses. + +:::warning +Core evaluates the address of the peer connected to it. It does not establish trust in `X-Forwarded-For`. Behind a reverse +proxy, Core normally sees the proxy address, not the original client address. Filter client IPs at a trusted proxy or +firewall; use Core's filter only for the backend or proxy addresses that connect directly to Core. Keep Core private and +use API-key authentication as defense in depth. +::: ## Before you start -:::caution no-title -This page is only relevant if you are self hosting SuperTokens. +:::warning +This page is only relevant if you are self-hosting SuperTokens. The option is not available if you are using the managed version of SuperTokens due to security reasons. In this case, you have to configure the filtering mechanism in your backend server. @@ -31,34 +30,31 @@ In this case, you have to configure the filtering mechanism in your backend serv ## Allow requests - - - + + ```bash docker run \ - -p 3567:3567 \ - -e IP_ALLOW_REGEX="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" \ - -d supertokens/supertokens- + --network app-network \ + -e IP_ALLOW_REGEX="^10\.0\.0\.12$" \ + -d "$SUPERTOKENS_IMAGE" ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command -ip_allow_regex: 127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1 +ip_allow_regex: '^10\.0\.0\.12$' ``` - - + + -The above only allows requests that originate from an IP that matches `127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1` regular expression. A breakdown of this regex is: -- `127\.\d+\.\d+\.\d+`: IPs that start with `127.`; OR -- `::1`: IPv6 from localhost; OR -- `0:0:0:0:0:0:0:1`: IPv6 from localhost +The example allows only a backend whose directly connected private address is exactly `10.0.0.12`. Replace it with a +stable private address assigned to your backend or trusted proxy. The anchors prevent partial matches and each dot is +escaped so that it means a literal dot. -In this way, only requests from localhost are allowed, and for other requests, the core returns a `403` status code. If instead you want to allow a list of IP addresses that correspond to your backend server's IP address, you can set this value to `IP1|IP2|IP3...` - for example: `100.12.12.3|192.167.4.3|50.32.5.1`. +To allow exact backend addresses, escape IPv4 dots and anchor the alternatives. For example: +`^(100\.12\.12\.3|192\.167\.4\.3|50\.32\.5\.1)$`. If this value is not set, then the core allows requests from any IP address. @@ -68,34 +64,29 @@ If this value is not set, then the core allows requests from any IP address. This is the opposite of the above configuration. If you only set this, the core allows requests from any IP other than the one that matches the regular expression corresponding to this setting. - - - + + ```bash docker run \ - -p 3567:3567 \ - // highlight-next-line - -e IP_DENY_REGEX="100.1.1.3" \ - -d supertokens/supertokens- + --network app-network \ + -e IP_DENY_REGEX="^10\.0\.0\.99$" \ + -d "$SUPERTOKENS_IMAGE" ``` - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command -ip_deny_regex: 100.1.1.3 +ip_deny_regex: '^10\.0\.0\.99$' ``` - - + + -The above setting makes the core accept requests from any IP other than `100.1.1.3`. For `100.1.1.3`, it returns a `403`. +The above setting makes Core accept requests from any directly connected peer other than exactly `10.0.0.99`. For that address, it returns a `403`. -:::info What if you set both the configurations? +:::info[What if you set both the configurations?] -In this case, the core allows requests only based on the value of `ip_allow_regex`, as long that request's IP doesn't match the regex of `ip_deny_regex`. For example, if you set `ip_allow_regex: IP1|IP2` and `ip_deny_regex: IP1`, then the core accepts requests only from `IP2`. +In this case, Core allows a request only if it matches `ip_allow_regex` and does not match `ip_deny_regex`. ::: - diff --git a/docs/platform-configuration/supertokens-core/meta.ts b/docs/platform-configuration/supertokens-core/meta.ts new file mode 100644 index 0000000000..472adf84d0 --- /dev/null +++ b/docs/platform-configuration/supertokens-core/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "SuperTokens Core", + icon: "cpu", + order: 1, +}); diff --git a/docs/post-authentication/_category_.json b/docs/post-authentication/_category_.json deleted file mode 100644 index dd4f9e45f4..0000000000 --- a/docs/post-authentication/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Post Authentication", - "collapsible": true, - "collapsed": true, - "customProps": { - "isMainCategory": true - }, - "position": 5 -} diff --git a/docs/post-authentication/account-linking/_blocks/paid-feature-callout.mdx b/docs/post-authentication/account-linking/_blocks/paid-feature-callout.mdx deleted file mode 100644 index ab021fe07f..0000000000 --- a/docs/post-authentication/account-linking/_blocks/paid-feature-callout.mdx +++ /dev/null @@ -1,10 +0,0 @@ - -:::warning Paid Feature - -This is a paid feature. - -For self hosted users, [Sign up](https://supertokens.com/auth) to get a license key and follow the instructions sent by email. Using the `dev` license key is free. Charging only starts once you enable the feature in production using the provided production license key. - -For managed service users, open the [SaaS Dashboard](https://supertokens.com/dashboard), select the relevant **Managed** deployment, and enable this feature from **Features**. Changes are saved automatically. Once enabled, this feature is free on the provided development environment. - -::: diff --git a/docs/post-authentication/account-linking/_category_.json b/docs/post-authentication/account-linking/_category_.json deleted file mode 100644 index a4570ca9fa..0000000000 --- a/docs/post-authentication/account-linking/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Account Linking", - "position": 3 -} diff --git a/docs/post-authentication/account-linking/add-passwords-to-an-existing-account.mdx b/docs/post-authentication/account-linking/add-passwords-to-an-existing-account.mdx index cb90a62df0..3ba950664b 100644 --- a/docs/post-authentication/account-linking/add-passwords-to-an-existing-account.mdx +++ b/docs/post-authentication/account-linking/add-passwords-to-an-existing-account.mdx @@ -1,16 +1,10 @@ --- title: Add passwords to an existing account -hide_title: true -sidebar_position: 7 description: Add a new password to an existing account using the account linking feature. -page_type: guide -recipe: accountlinking -category: account-linking +sidebar: + order: 7 --- - -# Add passwords to an existing account - ## Overview There may be scenarios in which you want to add a password to an account created using a social provider or passwordless login. @@ -26,171 +20,299 @@ Of course, there are security checks done to ensure there is no account takeover We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. -The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking and `emailpassword` on the backend SDK - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/types"; import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; -import EmailPassword from "supertokens-node/recipe/emailpassword" +import EmailPassword from "supertokens-node/recipe/emailpassword"; supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - EmailPassword.init(), - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { - if (user === undefined) { - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - } - if (session !== undefined && session.getUserId() === user.id) { - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - } - return { - shouldAutomaticallyLink: false - } - } - }) - // highlight-end - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init(), + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: any, + ) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + if (session !== undefined && session.getUserId() === user.id && session.getTenantId() === tenantId) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], }); ``` + + + + + +```python +from typing import Any, Dict, Optional, Union + +from supertokens_python.recipe import accountlinking, emailpassword +from supertokens_python.recipe.accountlinking.types import ( + AccountInfoWithRecipeIdAndUserId, + ShouldAutomaticallyLink, + ShouldNotAutomaticallyLink, +) +from supertokens_python.recipe.session.interfaces import SessionContainer +from supertokens_python.types import User + + +async def should_do_automatic_account_linking( + new_account_info: AccountInfoWithRecipeIdAndUserId, + user: Optional[User], + session: Optional[SessionContainer], + tenant_id: str, + user_context: Dict[str, Any], +) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: + if user is None: + return ShouldAutomaticallyLink(should_require_verification=True) + + if ( + session is not None + and session.get_user_id() == user.id + and session.get_tenant_id() == tenant_id + ): + return ShouldAutomaticallyLink(should_require_verification=True) + + return ShouldNotAutomaticallyLink() + + +recipe_list = [ + emailpassword.init(), + accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking + ), +] +``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -In the above implementation of `shouldDoAutomaticAccountLinking`, account linking is only allowed if the input session is present. This means that the system links an email password account to an existing session user. Otherwise, account linking is not allowed, which means that first factor account linking is not enabled. If you want to enable that too, you can see the [automatic account linking documentation](./automatic-account-linking). +The callback allows a new user to become a primary user when `user` is absent. It links to an existing user only when +the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking +between existing users during first-factor authentication. To enable that behavior, see the +[automatic account linking documentation](./automatic-account-linking). ### 2. Create a UI to show a password input to the user and handle the submit event -:::important -If you want to use password based auth as a second factor, or for step up auth, see the docs in the [MFA recipe](/docs/additional-verification/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. +:::note +If you want to use password based auth as a second factor, or for step up auth, see the docs in the [MFA recipe](/additional-verification/mfa/introduction) instead. The guide below is only meant for if you want to add a password for a user and allow them to login via email password for first factor login. ::: -First, you need to detect if there already exists a password for the user. You can do this by inspecting the [user object](/docs/references/backend-sdks/user-object) on the backend and checking if there is an `emailpassword` login method. +First, you need to detect if there already exists a password for the user. You can do this by inspecting the [user object](/references/backend-sdks/user-object) on the backend and checking if there is an `emailpassword` login method. -Then, if no such login method exists, you have to show a UI in which the user can add a password to their account. The [password validation documentation](/docs/authentication/email-password/customize-the-sign-up-form#change-field-validators#changing-the-default-email-and-password-validators) contains the default password validation rules. +Then, if no such login method exists, you have to show a UI in which the user can add a password to their account. The [password validation documentation](/authentication/email-password/customize-the-sign-up-form#change-field-validators) contains the default password validation rules. -You also need to fetch the email of the user before you call the email password sign up API. You can fetch this using the user object. If the `user` object does not have an email (which can only happen if the first factor is phone OTP), then you should ask the user to go through an email OTP flow via the passwordless recipe. This step should occur before asking them to set a password. The email OTP flow also results in creating a passwordless user account and linking it to the session user. +You also need to fetch a verified email for the current tenant before you call the email-password sign-up API. Fetch it +on the backend from a login method on the user object whose `tenantIds` contains the session tenant. Do not accept an +email from the client as proof of ownership. If no tenant-scoped, verified email exists, first complete an email OTP +flow through the passwordless recipe and link that login method to the same session user. Once you have the email on the frontend, you should call the sign up API. The two big differences in the implementation are: - When you call the sign up API, you need to provide the session's access token in the request. If you are using the frontend SDK, this process happens automatically via the frontend network interceptors. The access token enables the backend to get a session and then link the email password account to session user. -- New types of failure scenarios exist when calling the sign up API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). +- New types of failure scenarios exist when calling the sign up API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#error-status-codes) (> `ERR_CODE_008`). ### 3. Check for email match in the backend sign up API -Since the frontend specifies the email, verify it in the backend API before using it (since the frontend shouldn't be trusted). You can do this by overriding the email password sign up API: +Since the frontend specifies the email, verify its ownership on the backend before using it. The email must belong to a +verified login method for the session user in the request tenant. You can enforce this by overriding the email-password +sign-up API: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signUpPOST: async function (input) { - if (input.session !== undefined) { - // this means that we are trying to add a password to the session user - const inputEmail = input.formFields.find(f => f.id === "email")!.value; - let sessionUserId = input.session.getUserId(); - let userObject = await SuperTokens.getUser(sessionUserId); - if (userObject!.emails.find(e => e === inputEmail) === undefined) { - // this means that the input email does not belong to this user. - return { - status: "GENERAL_ERROR", - message: "Cannot use this email to add a password for this user" - } - } - } - return await originalImplementation.signUpPOST!(input); - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signUpPOST: async function (input) { + if (input.session !== undefined) { + // this means that we are trying to add a password to the session user + const inputEmail = input.formFields.find((field) => field.id === "email")?.value; + if (typeof inputEmail !== "string") { + return { + status: "GENERAL_ERROR", + message: "A valid email is required", + }; + } + const sessionUserId = input.session.getUserId(); + const tenantId = input.tenantId; + if (input.session.getTenantId() !== tenantId) { + return { + status: "GENERAL_ERROR", + message: "Cannot add a password across tenants", + }; } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + const userObject = await SuperTokens.getUser(sessionUserId); + const ownsVerifiedEmail = userObject?.loginMethods.some( + (loginMethod) => + loginMethod.tenantIds.includes(tenantId) && + loginMethod.verified && + loginMethod.hasSameEmailAs(inputEmail), + ); + if (!ownsVerifiedEmail) { + return { + status: "GENERAL_ERROR", + message: "Cannot use this email to add a password for this user", + }; + } + } + return await originalImplementation.signUpPOST!(input); + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - + + + + + +```python +from typing import Any, Dict, List, Optional, Union + +from supertokens_python.asyncio import get_user +from supertokens_python.recipe import emailpassword +from supertokens_python.recipe.emailpassword.interfaces import ( + APIInterface, + APIOptions, + EmailAlreadyExistsError, + SignUpPostNotAllowedResponse, + SignUpPostOkResult, +) +from supertokens_python.recipe.emailpassword.types import FormField +from supertokens_python.recipe.session.interfaces import SessionContainer +from supertokens_python.types import GeneralErrorResponse + + +def override_emailpassword_apis(original_implementation: APIInterface) -> APIInterface: + original_sign_up_post = original_implementation.sign_up_post + + async def sign_up_post( + form_fields: List[FormField], + tenant_id: str, + session: Optional[SessionContainer], + should_try_linking_with_session_user: Optional[bool], + api_options: APIOptions, + user_context: Dict[str, Any], + ) -> Union[ + SignUpPostOkResult, + EmailAlreadyExistsError, + SignUpPostNotAllowedResponse, + GeneralErrorResponse, + ]: + if session is not None: + input_email = next(field.value for field in form_fields if field.id == "email") + user = await get_user(session.get_user_id(), user_context) + owns_verified_email = user is not None and any( + tenant_id in login_method.tenant_ids + and login_method.verified + and login_method.has_same_email_as(input_email) + for login_method in user.login_methods + ) + if session.get_tenant_id() != tenant_id or not owns_verified_email: + return GeneralErrorResponse( + message="Cannot use this email to add a password for this user" + ) + + return await original_sign_up_post( + form_fields, + tenant_id, + session, + should_try_linking_with_session_user, + api_options, + user_context, + ) + + original_implementation.sign_up_post = sign_up_post + return original_implementation + + +emailpassword.init( + override=emailpassword.EmailPasswordOverrideConfig( + apis=override_emailpassword_apis + ) +) +``` + + --- ## See also - - - - - - + + + + + + diff --git a/docs/post-authentication/account-linking/automatic-account-linking.mdx b/docs/post-authentication/account-linking/automatic-account-linking.mdx index a4edbb8a92..d3de0973ee 100644 --- a/docs/post-authentication/account-linking/automatic-account-linking.mdx +++ b/docs/post-authentication/account-linking/automatic-account-linking.mdx @@ -1,27 +1,18 @@ --- title: Automatic account linking -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 -description: >- - Enable automatic account linking for multiple login methods with SuperTokens, - ensuring secure account management. -page_type: guide -recipe: accountlinking -category: account-linking +description: Enable automatic account linking for multiple login methods with SuperTokens, ensuring secure account management. +sidebar: + order: 3 --- - - -# Automatic account linking - ## Overview -Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. On a high level, SuperTokens automatically links the accounts for the different login methods provided that: +Automatic account linking is a feature that allows users to automatically sign in to their existing account using more than one login method. At a high level, SuperTokens can automatically link accounts for different login methods when: - Their emails or phone numbers are the same. -- They have verified their emails or phone numbers. +- The new login method has a verified identifier when your callback returns `shouldRequireVerification: true`. -SuperTokens ensures that accounts are automatically linked only if there is no risk of account takeover. +SuperTokens applies account-takeover checks before linking. Your callback remains part of that security boundary, +especially if you disable verification. ## Before you start @@ -35,9 +26,15 @@ SuperTokens ensures that accounts are automatically linked only if there is no r You can enable this feature by providing the following callback implementation on the backend SDK: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; @@ -45,57 +42,67 @@ import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/ import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; // Prevent account linking if the user already exists in your database -function checkIfUserHasAssociatedInformation(accountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined): boolean { - if(!accountInfo.recipeUserId || !user) return false; +function checkIfUserHasAssociatedInformation( + accountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, +): boolean { + if (!accountInfo.recipeUserId || !user) return false; const userId = accountInfo.recipeUserId.getAsString(); - const hasAssociatedInformation = false + const hasAssociatedInformation = false; return hasAssociatedInformation; } supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { - // This step is required if you are saving user information in your own database. - const hasAssociatedInformation = checkIfUserHasAssociatedInformation(newAccountInfo, user); - if (hasAssociatedInformation) { - return { - shouldAutomaticallyLink: false, - } - } - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - } - }) - // highlight-end - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: any, + ) => { + if ( + session !== undefined && + (user === undefined || session.getUserId() !== user.id || session.getTenantId() !== tenantId) + ) { + return { + shouldAutomaticallyLink: false, + }; + } + + // This step is required if you are saving user information in your own database. + const hasAssociatedInformation = checkIfUserHasAssociatedInformation(newAccountInfo, user); + if (hasAssociatedInformation) { + return { + shouldAutomaticallyLink: false, + }; + } + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + ], }); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - + + ```python from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking @@ -122,11 +129,18 @@ async def should_do_automatic_account_linking( tenant_id: str, user_context: Dict[str, Any] ) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: + if session is not None and ( + user is None + or session.get_user_id() != user.id + or session.get_tenant_id() != tenant_id + ): + return ShouldNotAutomaticallyLink() + has_associated_information = await check_if_user_has_associated_information(new_account_info, user) # This step is required if you are saving user information in your own database. if has_associated_information: return ShouldNotAutomaticallyLink() - + return ShouldAutomaticallyLink(should_require_verification=True) @@ -139,47 +153,52 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework='...', # type: ignore + framework="fastapi", recipe_list=[ accountlinking.init(should_do_automatic_account_linking=should_do_automatic_account_linking) ], ) ``` - - - + + - -## Input - + | Argument | Type | Description | |----------|------|-------------| -| `newAccountInfo` | `AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }` | Contains information about the user whose account is going to link or become a primary user. Includes email, social login info, phone number, and login method (`emailpassword`, `thirdparty`, or `passwordless`). May contain `recipeUserId` during account linking. Note: When `newAccountInfo.recipeUserId !== undefined && user !== undefined`, extra logic checks if the user ID has associated data in your application db to prevent data loss. | +| `newAccountInfo` | `AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }` | Contains information about the user whose account is going to link or become a primary user. Includes email, social login info, phone number, WebAuthn credential IDs, and login method (`emailpassword`, `thirdparty`, `passwordless`, or `webauthn`). May contain `recipeUserId` during account linking. When `newAccountInfo.recipeUserId !== undefined && user !== undefined`, check whether that recipe user ID has associated data in your application database to prevent data loss. | | `user` | `User \| undefined` | If not `undefined`, indicates `newAccountInfo` user links to this user. If `undefined`, `newAccountInfo` user becomes a primary user. | | `session` | `SessionContainerInterface \| undefined` | Session object of the user who is linking. `undefined` for first factor login. Defined if user completed first factor and calls sign up/in API again (MFA or social login linking). | -| `tenant` | `string` | ID of the tenant the user is signing in/up to. | -| `userContext` | `any` | User defined context object. | - -## Output - +| `tenantId` | `string` | ID of the tenant the user is signing in or signing up to. Account matching and linking are scoped to this tenant. | +| `userContext` | `any` | User-defined context object. | + + | Argument | Type | Description | |----------|------|-------------| | `shouldAutomaticallyLink` | `boolean` | If `true`, `newAccountInfo` links or becomes primary user during API call (subject to security checks). If `false`, no account linking operation occurs. | -| `shouldRequireVerification` | `boolean` | If `true`, account linking only happens if `newAccountInfo` verifies. **Strongly recommended to keep as `true` for security.** | - +| `shouldRequireVerification` | `boolean` | If `true`, account linking only happens after the new login method's matching identifier is verified. Keep this `true` unless your backend has independently verified ownership of that identifier; client-provided values are not ownership evidence. | +
-:::important -If you are returning `shouldRequireVerification` as `true`, then you need to also [enable the email verification recipe](/docs/additional-verification/email-verification/initial-setup) in `REQUIRED` mode. This means that if the login method does not inherently verify the email (like for email password login), SuperTokens requires the user to go through the email verification flow first. Then, it attempts auto linking of the account. For other login methods like sign in with Google, the email is already verified during login. The user does not need to verify the email again, and account linking occurs immediately. +:::note +If you return `shouldRequireVerification: true`, enable the +[email verification recipe](/additional-verification/email-verification/initial-setup). `REQUIRED` mode prevents access +until a login method that does not inherently verify its email, such as email-password, completes verification; linking +is then retried. A provider login whose backend-validated response marks the email verified can link immediately. If you enable email verification in `OPTIONAL` mode, the user can access the account after email password login. However, account linking only occurs after they verify their email later on. This is risky because while the user had access to their email password account after sign up, they could lose access after verification and account linking completes due to the change in the primary user ID. A callback is available to help migrate data from one user ID to another. ::: You can use the input of the function to dynamically decide if you want to do account linking for a particular user and / or login method or not. +Do not use a client-provided email, phone number, provider user ID, or WebAuthn credential ID to authorize linking. +Derive identifiers from the authenticated provider, WebAuthn ceremony, or backend user record. If `session` is present, +only authorize a session-driven link when `session.getUserId()` equals `user.id` and `session.getTenantId()` equals +`tenantId`. Returning `false` preserves both existing users; conflict statuses never transfer a login method between +primary users. + ## References @@ -204,7 +223,7 @@ If the current user is not already linked and if there exists another user with If the current user whose email got verified is not a primary user, and there exists another primary user in the same tenant with the same email, then the two accounts link if: - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. -:::info no-title +:::info For a primary user, if two login methods (L1 & L2) share the same email, but L1's email verifies and L2's does not, SuperTokens automatically verifies L2's email under these conditions: - The user logs in with L2. - The `updateEmailOrPassword` (email password recipe) or `updateUser` (passwordless recipe) function calls to update L2's email to match L1's. @@ -217,7 +236,7 @@ If there already exists a user with the same email in a non email password recip - Your implementation for `shouldDoAutomaticAccountLinking` returns `true` for the `shouldAutomaticallyLink` boolean. -:::info Email update implications +:::info[Email update implications] When updating a user's login email, SuperTokens ensures account linking conditions remain valid. A primary user's email cannot update to match another primary user's email. @@ -245,94 +264,111 @@ If there was any older data associated with User B (against user ID `p2`), in yo To prevent this scenario, you should: - Make sure that you return `false` for `shouldAutomaticallyLink` boolean in the `shouldDoAutomaticAccountLinking` function implementation if there exists a `recipeUserId` in the `newAccountInfo` object, and if you have some information related to that user ID in your own database. -This appears in the [code snippet above](#enabling-automatic-account-linking). +This appears in the [code snippet above](#1-enable-the-recipe). - If you do not want to return `false` in this case, and want the accounts to link, then make sure to implement the `onAccountLinked` callback:
- ```tsx - import supertokens, { User, RecipeUserId } from "supertokens-node" - import AccountLinking from "supertokens-node/recipe/accountlinking"; - import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; - import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; - - supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - }, - // highlight-start - onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { - let olderUserId = newAccountInfo.recipeUserId.getAsString() - let newUserId = user.id; - - // TODO: migrate data from olderUserId to newUserId in your database... - } - // highlight-end - }) - ] - }); - ``` -:::caution -If your logic in `onAccountLinked` throws an error, then it is not called again, and still results in linking the accounts. However, the end user would see an error on the UI as the API returns a `500` status code. They can retry the login action and log into the primary user's account as expected. +```tsx +import supertokens, { User, RecipeUserId } from "supertokens-node"; +import AccountLinking from "supertokens-node/recipe/accountlinking"; +import { AccountInfoWithRecipeId, RecipeLevelUser } from "supertokens-node/recipe/accountlinking/types"; +import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; + +supertokens.init({ + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: any, + ) => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + onAccountLinked: async (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => { + let olderUserId = newAccountInfo.recipeUserId.getAsString(); + let newUserId = user.id; + + // TODO: migrate data from olderUserId to newUserId in your database... + }, + }), + ], +}); +``` + +:::warning[`onAccountLinked` does not provide rollback] +SuperTokens calls `onAccountLinked` after the Core has linked the accounts. If the callback throws, the link remains, +the callback is not retried automatically, and the API returns `500`. Make data migration idempotent and record enough +state to reconcile failures asynchronously. Retrying login does not rerun this callback for an already-linked account. ::: ### Error status codes -The following is a list of error status codes that the end user might see during their interaction with the login UI (as a general error message in the pre-built UI). +The following codes can appear in general errors shown by the pre-built UI. Released Node.js 24.0.3 uses these families: - +| Recipe or operation | Codes | +|---|---| +| Password reset/recovery protection | `ERR_CODE_001` | +| Passwordless sign-in/up and session linking | `ERR_CODE_002`, `ERR_CODE_003`, `ERR_CODE_017`–`ERR_CODE_019` | +| Third-party sign-in/up and session linking | `ERR_CODE_004`–`ERR_CODE_006`, `ERR_CODE_020`–`ERR_CODE_024` | +| Email-password sign-in/up and session linking | `ERR_CODE_007`–`ERR_CODE_016` | +| WebAuthn sign-up and session linking | `ERR_CODE_025`–`ERR_CODE_029` | +| WebAuthn sign-in and session linking | `ERR_CODE_030`–`ERR_CODE_034` | -## `ERR_CODE_001` +For session-linking families, the consecutive codes distinguish verification required, a recipe user already linked to +another primary user, account information already associated with another primary user, and session-user account +information already associated with another primary user. Do not parse the message text; handle the API status and show +the reason as a support-safe error. + + - This can happen during creating a password reset code in the email password flow: - - API path and method: `/user/password/reset/token POST` + - API path and method: `/user/password/reset/token POST` - Output JSON: ```json { - "status": "PASSWORD_RESET_NOT_ALLOWED", - "reason": - "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" + "status": "PASSWORD_RESET_NOT_ALLOWED", + "reason": "Reset password link was not created because of account take over risk. Please contact support. (ERR_CODE_001)" } ``` - The pre-built UI on the frontend displays this error in the following way: - pre-built UI screenshot showing error message for ERR_CODE_001. + pre-built UI screenshot showing error message for ERR_CODE_001. - Below is the scenario for when this status returns: - + A malicious user, User A, which is a primary user, has login methods with email `e1` (social login) and email `e1` (`emailpassword` login). If user A changes their `emailpassword` email to `e2` (which is in unverified state), and the real user of `e2` (the victim) tries to sign up via email password, they see a message saying that the email already exists. The victim may then try to do a password reset (thinking they had previously signed up). If this happens, and the victim resets the password (since they are the real owner of the email), then they can login to the account, and the attacker can spy on what the user is doing via their third party login method. To prevent this scenario, enforcement ensures that the password link is only generated if the primary user has at least one login method that has the input email ID and verifies it, or if not, checks that the primary user has no other login method with a different email, or phone number. If these cases are not satisfied, then the system returns the error code `ERR_CODE_001`. - To resolve this, you would have to manually verify the user's identity and check that they own each of the emails / phone numbers associated with the primary user. Once verified, you can manually mark the email from the email password account as verified, and then ask them to go through the password reset flow once again. If they do not own each of the emails / phone numbers associated with the account, you can manually unlink the login methods which they do not own, and then ask them to go through the password reset flow once again. **You can do these actions using the user management dashboard.** - -## `ERR_CODE_002` - + + - This can happen during the passwordless recipe's create or consume code API (during sign up): - API path and method: `/signinup/code POST` or `/signinup/code/consume POST` - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_002)" } ``` - The pre-built UI on the frontend displays this error in the following way: - pre-built UI screenshot showing error message for ERR_CODE_002. + pre-built UI screenshot showing error message for ERR_CODE_002. - Below is an example scenario for when this status returns (one amongst many): A user is trying to sign up using passwordless login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If this scenario occurs, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the passwordless login method. This way, the attacker gains access to the user's account. @@ -340,63 +376,62 @@ A user is trying to sign up using passwordless login method with email `e1`. The To prevent this, sign up with passwordless login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then mark their email as verified in the other account that has the same email, before asking them to retry passwordless login. **You can do these actions using the user management dashboard.** - - -## `ERR_CODE_003` - -This used to be an error code which is no longer valid and you can ignore it. - -## `ERR_CODE_004` - + + +- This can happen during passwordless code consumption when sign-in is blocked to prevent unsafe account linking: + - API path and method: `/signinup/code/consume POST` + - Output status: `SIGN_IN_UP_NOT_ALLOWED` +- Ask the user to use another login method that is already associated with the account or contact support. Do not bypass + the check based on client-provided account information. + + - This can happen during the third party recipe's `/signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please try a different login method or contact support. (ERR_CODE_004)" } ``` - The pre-built UI on the frontend displays this error in the following way: - Pre-built UI screenshot showing error for message ERR_CODE_004. + Pre-built UI screenshot showing error for message ERR_CODE_004. - Below is an example scenario for when this status returns (one amongst many): There exists a `thirdparty` user with email `e1`, sign in with Google (owned by the victim, and the email is verified). There exists another `thirdparty` login method with email, `e2` (owned by an attacker), such as login with GitHub. The attacker then goes to their GitHub and changes their email to `e1` (which is in unverified state). The next time the attacker tries to login, via GitHub, they see this error code. Login is prevented, because if it wasn't, then the attacker might send an email verification link to `e1`, and if the victim clicks on it, then the attacker's account will link to the victim's account. - To resolve this issue, you can delete the login method that has the unverified email, or if manually mark the unverified account as verified (if you confirm the identity of its owner). **You can do these actions using the user management dashboard.** - -## `ERR_CODE_005` - + + - This can happen during the third party recipe's `signinup` API (during sign in): - API path and method: `/signinup POST` - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_005)" } ``` - The pre-built UI on the frontend displays this error in the following way: - Pre-built UI screenshot showing error message for ERR_CODE_005. + Pre-built UI screenshot showing error message for ERR_CODE_005. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, third party user with email `e1`, sign in with Google. There exists another email password user with email `e2`, which is a primary user. If the user changes their email on Google to `e2`, and then try logging in via Google, they see this error code. This occurs because if it wasn't, then it would result in two primary users having the same email, which violates one of the account linking rules. - To resolve this issue, you can make one of the primary users as non primary (use the unlink button against the login method on the user management dashboard). Once the user is not a primary user, you can ask the user to re-login with that method, and it should auto link that account with the existing primary user. - -## `ERR_CODE_006` - + + - This can happen during the third party recipe's `signinup` API (during sign up): - API path and method: `/signinup POST` - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_006)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up because new email cannot be applied to existing account. Please contact support. (ERR_CODE_006)" } ``` - The pre-built UI on the frontend displays this error in the following way: - Pre-built UI screenshot showing error message for ERR_CODE_006. + Pre-built UI screenshot showing error message for ERR_CODE_006. - Below is as example scenario for when this status returns (one amongst many): A user is trying to sign up using third party login method with email `e1`. There exists an email password login method with `e1`, which remains unverified (owned by an attacker). If the third party sign up is allowed, and then the attacker initiates the email verification flow for the email password method, the real user might click on the verification email (since they signed up, they do not get suspicious), and then the attacker's login method links to the third party login method. This way, the attacker has access to the user's account. @@ -404,54 +439,51 @@ A user is trying to sign up using third party login method with email `e1`. Ther To prevent this, sign up with third party login is not allowed in case there exists another account with the same email and remains unverified. - To resolve this issue, you should ask the user to try another login method (which already has their email), or then manually mark their email as verified in the other account that has the same email, before asking them to retry third party login. **You can do these actions using the user management dashboard.** - -## `ERR_CODE_007` - + + - This can happen during the email password sign up API: - API path and method: `/signup POST` - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)" } ``` - The pre-built UI on the frontend displays this error in the following way: - Pre-built UI screenshot showing error message for ERR_CODE_007. + Pre-built UI screenshot showing error message for ERR_CODE_007. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. If an attacker tries to sign up with email password with email `e1`, the system sends an email verification email to the victim, and they may click it since they had previously signed up with Google. This links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try and login, or go through the reset password flow. - -## `ERR_CODE_008` - + + - This can happen during the email password sign in API: - API path and method: `/signin POST` - Output JSON: ```json { - "status": "SIGN_IN_NOT_ALLOWED", - "reason": "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)" + "status": "SIGN_IN_NOT_ALLOWED", + "reason": "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)" } ``` - The pre-built UI on the frontend displays this error in the following way: - Pre-built UI screenshot showing error message for ERR_CODE_008. + Pre-built UI screenshot showing error message for ERR_CODE_008. - Below is as example scenario for when this status returns (one amongst many): There exists a primary, social login account with email `e1`, sign in with Google. There also exists an email password account (owned by the attacker) that remains unverified with the same email `e1` (this is not a primary user). If the attacker tries to sign in with email password, they see this error. This occurs because if it wasn't, then the attacker might send an email verification email on sign in, and the actual user may click on it (since they had previously signed up). Upon verifying that account, the system links the attacker's account to the victim's account. - To resolve this issue, you can ask the user to try the reset password flow. - -## `ERR_CODE_014` - + + - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_014)" } ``` - An example scenario of when in the following scenario: @@ -460,16 +492,15 @@ There exists a primary, social login account with email `e1`, sign in with Googl - The user logs out, and then creates a social login account with email `e1`. Then, they receive a request to add a password to this account. Since an email password account with `e1` already exists, SuperTokens tries and links that to this new account, but fails, since the email password account with `e1` is already a primary user. - To resolve this, it is recommended to manually link the `e1` social login account with the `e1` email password account. Alternatively, enable automatic account linking for first factor to prevent the above scenario. - -## `ERR_CODE_015` - + + - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_015)" } ``` - An example scenario of when in the following scenario: @@ -479,106 +510,101 @@ There exists a primary, social login account with email `e1`, sign in with Googl - This causes this type of error since the linking of the new social login and email account fails since there already exists another primary user with the same (`e1`) email. - To resolve this, it is recommended not allowing users to specify an email when asking them to add a password for their account. - -## `ERR_CODE_016` - + + - This can happen when adding a password to an existing session user: - API Path is `/signup POST`. - Output JSON: ```json { - "status": "SIGN_UP_NOT_ALLOWED", - "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" + "status": "SIGN_UP_NOT_ALLOWED", + "reason": "Cannot sign up due to security reasons. Please contact support. (ERR_CODE_016)" } ``` - An example scenario of when in the following scenario: - Let's say that the app is configured to not have automatic account linking during the first factor. - A user signs up with a social login account using Google with email `e1`, and they add another social account, with Facebook, with the same email. - - The user logs out and creates another social login account with email `e1` (say GitHub), and then tries and adds a password to this account with email `e1`. Here, SuperTokens tries and makes the GitHub login a primary user, but fails, since the email `e1` is already a primary user (with Google login). + - The user logs out and creates another social login account with email `e1` (say GitHub), and then tries and adds a password to this account with email `e1`. Here, SuperTokens tries and makes the GitHub login a primary user, but fails, since the email `e1` is already a primary user (with Google login). - To resolve this, it is recommended that you manually link the `e1` GitHub social login account with the `e1` Google social login account. Or you can enable automatic account linking for first factor and this way, the above scenario will not happen. - -## `ERR_CODE_020` - + + - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_020)" } ``` -- This can happen when the third party account that is trying to link to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). -- To resolve this, you can return `shouldRequireVerification` as `false` in the `shouldDoAutomaticAccountLinking` function implementation, or you can only allow users to link social login accounts that give verified accounts. - -## `ERR_CODE_021` - +- This can happen when the third party account that is trying to link to the session's account is not verified. It could happen when you are trying to associate a social login account to a user, but that social account's email is not verified (and if the email of that account is not the same as the current session's account's email). +- Only allow users to link provider accounts whose identifiers the provider marks as verified. Return + `shouldRequireVerification: false` only if your backend has independently verified ownership; client input is not + sufficient evidence. + + - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_021)" } ``` - This can happen when the third party account that is trying to link to the session's account is already linked with another primary user. - -## `ERR_CODE_022` - + + - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_022)" } ``` -- This can happen when the third party account that is trying to link to the session's account has the same email as another primary user. - -## `ERR_CODE_023` - +- This can happen when the third party account that is trying to link to the session's account has the same email as another primary user. + + - This can happen during association of a third party login to an existing session's account. - API Path is `/signinup POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_023)" } ``` - To link the third party user with the session user, we need to make sure that the session user is a primary user. However, that can fail if there exists another primary user with the same email as the session user, and in this case, this error returns to the frontend. - -## `ERR_CODE_024` - -- This happens during third party sign in, when the user is trying to sign in with a non-primary user, and the third party provider does not verify their email, and their exists a primary user with the same email. This can also happen the other way around wherein the user is trying to sign in with the primary user (unverified email), and there exists a non-primary user with the same email. + + +- This happens during third party sign in, when the user is trying to sign in with a non-primary user, and the third party provider does not verify their email, and their exists a primary user with the same email. This can also happen the other way around wherein the user is trying to sign in with the primary user (unverified email), and there exists a non-primary user with the same email. - API Path is `/signinup POST`. - Output JSON: ```json { - "status": "SIGN_IN_UP_NOT_ALLOWED", - "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_024)" + "status": "SIGN_IN_UP_NOT_ALLOWED", + "reason": "Cannot sign in / up due to security reasons. Please contact support. (ERR_CODE_024)" } ``` - You can resolve this by deleting the (non primary) user that has the same email ID, or by manually marking the email of the user as verified for the login method that they are trying to sign in with. - - + #### Changing the error message on the frontend -If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](/docs/references/frontend-sdks/prebuilt-ui/translations). +If you want to display a different message to the user, or use a different status code, you can change them on the frontend via [the language translation feature](/references/frontend-sdks/prebuilt-ui/translations). --- ## See also - - - - - - - + + + + + + + diff --git a/docs/post-authentication/account-linking/important-concepts.mdx b/docs/post-authentication/account-linking/important-concepts.mdx index 7031941945..09a9681aaa 100644 --- a/docs/post-authentication/account-linking/important-concepts.mdx +++ b/docs/post-authentication/account-linking/important-concepts.mdx @@ -1,17 +1,10 @@ --- title: Important concepts -hide_title: true -sidebar_position: 2 -description: >- - Link multiple authentication methods to a single account, ensuring security - and user management. -page_type: guide -recipe: accountlinking -category: account-linking +description: Link multiple authentication methods to a single account, ensuring security and user management. +sidebar: + order: 2 --- -# Important concepts - ## Overview The following page describes concepts that are relevant towards understanding how account linking works in **SuperTokens**. @@ -28,7 +21,7 @@ The primary user ID remains constant when accounts link to it. A user can become a primary user only if no other primary users share the same email, third-party information, or phone number across all tenants. This applies to all tenants to which the user belongs. Hence, two primary users with the same email address, one using email/password login and the other using social login. -:::info Multi-tenancy +:::info[Multi-tenancy] Additionally, the following scenario is not permitted in a multi-tenant context: - User A is a primary user with email `test@example.com` and belongs to tenants `t1` and `t2`. - User B is a primary user with email `test@example.com` and belongs to tenant `t2`. @@ -72,8 +65,7 @@ Only User B remains, which is a social login user, and its primary user ID equal Any metadata, role, sessions info continues to exist. 3. If unlinking a User A which is a primary user ID, but it has not linked users, it results in this user becoming a non-primary user. -:::important -All the above checks happen automatically. You don't need to worry about them. But it is important to understand what's happening. +:::note[All the above checks happen automatically. You don't need to worry about them. But it is important to understand what's happening.] ::: ## Security @@ -186,9 +178,9 @@ This causes the system to link the new, malicious account, thereby giving the ma ## See also - - - - - - + + + + + + diff --git a/docs/post-authentication/account-linking/introduction.mdx b/docs/post-authentication/account-linking/introduction.mdx index 6035f39408..8b0b4db7fe 100644 --- a/docs/post-authentication/account-linking/introduction.mdx +++ b/docs/post-authentication/account-linking/introduction.mdx @@ -1,19 +1,14 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true description: 'Learn how to manage users in SuperTokens ' -page_type: overview -recipe: accountlinking -category: account-linking +sidebar: + order: 1 --- - -# User management - ## Overview + + Account linking is the process of associating multiple authentication methods with the same account. For example, a user may have a password-based account and a Google account. They may want to link both of these accounts to the same user account in your application. @@ -22,62 +17,32 @@ The first method happens during user sign up. If a user signs up with a second login method with the same email or phone, the two accounts are automatically linked. The other one involves you setting up the linking process yourself. - -## Prerequisites - - - ## Getting started Before you going into the tutorials, read through the **Important concepts** page. It teaches you about the main things that you need to know in the context of account linking. After that, based on your use case, check either the automatic or manual account linking guides. - - - - Important concepts - - - Learn about the main concepts that you need to know in the context of account linking. - - - - - Automatic account linking - - - See how to enable and use the automatic account linking feature. - - - - - Manual account linking - - - Implement the account linking feature manually. - - - + + +Learn about the main concepts that you need to know in the context of account linking. + + +See how to enable and use the automatic account linking feature. + + +Implement the account linking feature manually. + + ## Customization - - - - Link social accounts - - - See how you can manually link multiple social accounts under the same user. - - - - - Add passwords to an existing account - - - See how you can add passwords to an existing account. - - - + + +See how you can manually link multiple social accounts under the same user. + + +See how you can add passwords to an existing account. + + diff --git a/docs/post-authentication/account-linking/link-social-accounts.mdx b/docs/post-authentication/account-linking/link-social-accounts.mdx index 99b9bf73a9..e83e4b8db0 100644 --- a/docs/post-authentication/account-linking/link-social-accounts.mdx +++ b/docs/post-authentication/account-linking/link-social-accounts.mdx @@ -1,16 +1,10 @@ --- title: Link social accounts -hide_title: true -sidebar_position: 6 -description: Link social accounts accounts using the account linking feature. -page_type: guide -recipe: accountlinking -category: account-linking +description: Link social accounts using the account linking feature. +sidebar: + order: 6 --- - -# Link social accounts - ## Overview The following guide shows you how to link a social account to an existing user account. @@ -26,17 +20,22 @@ Of course, there are security checks done to ensure there is no account takeover We do not provide pre-built UI for this flow since it's probably something you want to add in your settings page or during the sign up process. This guide focuses on which APIs to call from your own UI. -The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. - +The frontend code snippets below refer to the `supertokens-web-js` SDK. You can continue to use this even if you have initialised the `supertokens-auth-react` SDK, on the frontend. ## Steps ### 1. Enable account linking on the backend SDK - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; @@ -44,167 +43,278 @@ import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/ import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - AccountLinking.init({ - shouldDoAutomaticAccountLinking: async (newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, session: SessionContainerInterface | undefined, tenantId: string, userContext: any) => { - if (user === undefined) { - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - } - if (session !== undefined && session.getUserId() === user.id) { - return { - shouldAutomaticallyLink: true, - shouldRequireVerification: true - } - } - return { - shouldAutomaticallyLink: false - } - } - }) - // highlight-end - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async ( + newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, + user: User | undefined, + session: SessionContainerInterface | undefined, + tenantId: string, + userContext: any, + ) => { + if (user === undefined) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + if (session !== undefined && session.getUserId() === user.id && session.getTenantId() === tenantId) { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + } + return { + shouldAutomaticallyLink: false, + }; + }, + }), + ], }); ``` + + + + + +```python +from typing import Any, Dict, Optional, Union + +from supertokens_python.recipe import accountlinking +from supertokens_python.recipe.accountlinking.types import ( + AccountInfoWithRecipeIdAndUserId, + ShouldAutomaticallyLink, + ShouldNotAutomaticallyLink, +) +from supertokens_python.recipe.session.interfaces import SessionContainer +from supertokens_python.types import User + + +async def should_do_automatic_account_linking( + new_account_info: AccountInfoWithRecipeIdAndUserId, + user: Optional[User], + session: Optional[SessionContainer], + tenant_id: str, + user_context: Dict[str, Any], +) -> Union[ShouldNotAutomaticallyLink, ShouldAutomaticallyLink]: + if user is None: + return ShouldAutomaticallyLink(should_require_verification=True) + + if ( + session is not None + and session.get_user_id() == user.id + and session.get_tenant_id() == tenant_id + ): + return ShouldAutomaticallyLink(should_require_verification=True) + + return ShouldNotAutomaticallyLink() + + +accountlinking.init( + should_do_automatic_account_linking=should_do_automatic_account_linking +) +``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -In the above implementation of `shouldDoAutomaticAccountLinking`, account linking is only allowed if the input session is present. This means that the process is trying to link a social login account to an existing session user. Otherwise, account linking is not allowed, which means that first factor account linking does not occur. If you want to enable that too, you can see [the automatic account linking page](./automatic-account-linking). +The callback allows a new user to become a primary user when `user` is absent. It links to an existing user only when +the session user and tenant match the proposed primary user and current tenant. It therefore does not enable linking +between existing users during first-factor authentication. To enable that behavior, see +[the automatic account linking page](./automatic-account-linking). ### 2. Create a UI to show social login buttons and handle login -First, you need to detect which social login methods are already linked to the user. You can do this by inspecting the [user object](/docs/references/backend-sdks/user-object) on the backend and checking the `thirdParty.id` property (the values are like `google`, `facebook` etc). +First, you need to detect which social login methods are already linked to the user. You can do this by inspecting the [user object](/references/backend-sdks/user-object) on the backend and checking the `thirdParty.id` property (the values are like `google`, `facebook` etc). Then you have to create your own UI which asks the user to pick a social login provider to connect to. Once they click on one, redirect them to that provider's page. After login, the provider redirects the user back to your application (on the same path as the first factor login). You then call the APIs to consume the OAuth tokens and link the user. -The exact implementation of the above is available [in the initial setup documentation](/docs/authentication/social/initial-setup). The two big differences in the implementation are: +The exact implementation of the above is available [in the initial setup documentation](/authentication/social/initial-setup). The two big differences in the implementation are: - When you call the `signinup` API, you need to provide the session's access token in the request. If you are using the frontend SDK, the frontend network interceptors automatically handle this. The access token enables the backend to get a session and then link the social login account to session user. -- New types of failure scenarios exist when calling the `signinup` API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#err_code_001) (> `ERR_CODE_008`). +- New types of failure scenarios exist when calling the `signinup` API which are impossible during first factor login. To learn more about them, see the [error codes section](./automatic-account-linking#error-status-codes) (> `ERR_CODE_008`). -### 3. Remove the social login access token and user profile info on the backend +### 3. Access the social login access token and user profile on the backend Once you call the `signinup` API from the frontend, SuperTokens verifies the OAuth tokens and fetches the user's profile info from the third party provider. SuperTokens also links the newly created recipe user to the session user. To fetch the new user object and also the third party profile, you can override the `signinup` recipe function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import SuperTokens, { User } from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - // override the thirdparty sign in / up function - signInUp: async function (input) { - - let existingUser: User | undefined = undefined; - if (input.session !== undefined) { - existingUser = await SuperTokens.getUser(input.session.getUserId()); - } - - - let response = await originalImplementation.signInUp(input); - - if (response.status === "OK") { - - let accessToken = response.oAuthTokens["access_token"]; - - let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; - - if (input.session !== undefined && response.user.id === input.session.getUserId()) { - if (response.user.loginMethods.length === existingUser!.loginMethods.length + 1) { - // new social account was linked to session user - } else { - // social account was already linked to the session - // user from before - } - } - } - - return response; - } - } + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // override the thirdparty sign in / up function + signInUp: async function (input) { + let existingUser: User | undefined; + if (input.session !== undefined && input.session.getTenantId() === input.tenantId) { + existingUser = await SuperTokens.getUser(input.session.getUserId()); + } + + let response = await originalImplementation.signInUp(input); + + if (response.status === "OK") { + let accessToken = response.oAuthTokens["access_token"]; + + let firstName = response.rawUserInfoFromProvider.fromUserInfoAPI!["first_name"]; + + if ( + input.session !== undefined && + input.session.getTenantId() === input.tenantId && + response.user.id === input.session.getUserId() && + existingUser !== undefined + ) { + if (response.user.loginMethods.length === existingUser.loginMethods.length + 1) { + // new social account was linked to session user + } else { + // social account was already linked to the session + // user from before + } } - } - // highlight-end - }), - Session.init({ /* ... */ }) - ] + } + + return response; + }, + }; + }, + }, + }), + Session.init({ + /* ... */ + }), + ], }); ``` + + + + + +```python +from typing import Any, Dict, Optional + +from supertokens_python.recipe import thirdparty +from supertokens_python.recipe.session.interfaces import SessionContainer +from supertokens_python.recipe.thirdparty.interfaces import ( + RecipeInterface, + SignInUpOkResult, +) +from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider + + +def override_thirdparty_functions( + original_implementation: RecipeInterface, +) -> RecipeInterface: + original_sign_in_up = original_implementation.sign_in_up + + async def sign_in_up( + third_party_id: str, + third_party_user_id: str, + email: str, + is_verified: bool, + oauth_tokens: Dict[str, Any], + raw_user_info_from_provider: RawUserInfoFromProvider, + session: Optional[SessionContainer], + should_try_linking_with_session_user: Optional[bool], + tenant_id: str, + user_context: Dict[str, Any], + ): + existing_login_method_count = None + if session is not None and session.get_tenant_id() == tenant_id: + from supertokens_python.asyncio import get_user + + existing_user = await get_user(session.get_user_id(), user_context) + if existing_user is not None: + existing_login_method_count = len(existing_user.login_methods) + + result = await original_sign_in_up( + third_party_id, + third_party_user_id, + email, + is_verified, + oauth_tokens, + raw_user_info_from_provider, + session, + should_try_linking_with_session_user, + tenant_id, + user_context, + ) + + if ( + isinstance(result, SignInUpOkResult) + and session is not None + and session.get_tenant_id() == tenant_id + and result.user.id == session.get_user_id() + and existing_login_method_count is not None + ): + _access_token = result.oauth_tokens.get("access_token") + _provider_profile = result.raw_user_info_from_provider.from_user_info_api + + if len(result.user.login_methods) == existing_login_method_count + 1: + pass # The provider account was linked to this session user. + else: + pass # The provider account was already linked to this session user. + + return result + + original_implementation.sign_in_up = sign_in_up + return original_implementation + + +thirdparty.init( + override=thirdparty.ThirdPartyOverrideConfig( + functions=override_thirdparty_functions + ) +) +``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -Notice in the above snippet that the check is for `input.session !== undefined && response.user.id === input.session.getUserId()`. This ensures that the custom logic runs only if it's linking a social account to your session account, and not during first factor login. +The checks bind custom logic to the same session user and tenant. The provider identifiers, tokens, and profile in these +function results come from the backend-verified OAuth exchange; never use client-submitted provider identifiers as proof +that the current user owns a social account. A conflict leaves the provider login method linked to its existing primary +user and returns a linking error instead of moving it to the session user. --- ## See also - - - - - - - - + + + + + + + diff --git a/docs/post-authentication/account-linking/manual-account-linking.mdx b/docs/post-authentication/account-linking/manual-account-linking.mdx index ca5fb8c2f4..e53316f9a0 100644 --- a/docs/post-authentication/account-linking/manual-account-linking.mdx +++ b/docs/post-authentication/account-linking/manual-account-linking.mdx @@ -1,38 +1,34 @@ --- title: Manual account linking -hide_title: true -sidebar_position: 4 -description: >- - Manual account linking enables control over linking accounts with different - identifiers or post-login. -page_type: guide -recipe: accountlinking -category: account-linking +description: Manual account linking enables control over linking accounts with different identifiers or post-login. +sidebar: + order: 4 --- - -# Manual account linking - ## Overview + + Manual account linking allows you to take control of when and which accounts link. With this, you can implement flows like: - Connecting social login accounts to an existing account post login. - Adding a password to an account that a social or passwordless login created. - Linking accounts which don't have the same email or phone number, or have a different identifier altogether. -## Before you start - - - ## Steps ### 1. Initialize the account linking recipe - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import supertokens, { User, RecipeUserId } from "supertokens-node"; import AccountLinking from "supertokens-node/recipe/accountlinking"; @@ -40,34 +36,24 @@ import { AccountInfoWithRecipeId } from "supertokens-node/recipe/accountlinking/ import { SessionContainerInterface } from "supertokens-node/recipe/session/types"; supertokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // highlight-start - AccountLinking.init() - // highlight-end - ] + supertokens: { + connectionURI: "...", + apiKey: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [AccountLinking.init()], }); ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import accountlinking @@ -81,15 +67,14 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", ), - framework='...', # type: ignore + framework='...', recipe_list=[ accountlinking.init() ], ) ``` - - - + + In the above, SuperTokens does not automatically link accounts (during sign up or sign in APIs) by returning `shouldAutomaticallyLink: false`. Initializing the recipe is still important to use the functions from the SDK as shown below. @@ -99,45 +84,44 @@ It is of course possible to [enable auto account linking](./automatic-account-li To link two accounts, you first need to make one of them a primary user: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; -import {RecipeUserId} from "supertokens-node"; +import { RecipeUserId } from "supertokens-node"; async function makeUserPrimary(recipeUserId: RecipeUserId) { - let response = await AccountLinking.createPrimaryUser(recipeUserId); - if (response.status === "OK") { - if (response.wasAlreadyAPrimaryUser) { - // The input user was already a primary user and accounts can be linked to it. - } else { - // User is now primary and accounts can be linked to it. - } - let modifiedUser = response.user; - console.log(modifiedUser.isPrimaryUser); // will print true - } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { - // This happens if there already exists another primary user with the same email or phone number - // in at least one of the tenants that this user belongs to. - } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { - // This happens if this user is already linked to another primary user. + let response = await AccountLinking.createPrimaryUser(recipeUserId); + if (response.status === "OK") { + if (response.wasAlreadyAPrimaryUser) { + // The input user was already a primary user and accounts can be linked to it. + } else { + // User is now primary and accounts can be linked to it. } + let modifiedUser = response.user; + console.log(modifiedUser.isPrimaryUser); // will print true + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // in at least one of the tenants that this user belongs to. + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + // This happens if this user is already linked to another primary user. + } } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.accountlinking.asyncio import create_primary_user from supertokens_python.types import RecipeUserId @@ -155,16 +139,14 @@ async def make_user_primary(recipe_user_id: RecipeUserId): print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number - # in at least one of the tenants that this user belongs to. + # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` - - - - + + ```python from supertokens_python.recipe.accountlinking.syncio import create_primary_user from supertokens_python.types import RecipeUserId @@ -182,67 +164,64 @@ def make_user_primary(recipe_user_id: RecipeUserId): print(modified_user.is_primary_user) # will print True elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number - # in at least one of the tenants that this user belongs to. + # in at least one of the tenants that this user belongs to. pass elif response.status == "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR": # This happens if this user is already linked to another primary user. pass ``` - - - - - - + + + + ### 3. Link accounts Once a user has become a primary user, you can link other accounts to this user: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; import { RecipeUserId } from "supertokens-node"; // we are linking the input recipeUserId to the primaryUserId async function linkAccounts(primaryUserId: string, recipeUserId: RecipeUserId) { - let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); - if (response.status === "OK") { - if (response.accountsAlreadyLinked) { - // The input users were already linked - } else { - // The two users are now linked - } - let modifiedUser = response.user; - console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. - } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { - // This happens if there already exists another primary user with the same email or phone number - // as the recipeUserId's account. - } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { - // This happens if the input primaryUserId is not actually a primary user ID. - // You can call createPrimaryUserId and call linkAccountsAgain - } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { - // This happens if the input recipe user ID is already linked to another primary user. - // You can call unlink accounts on the recipe user ID and then try linking again. + let response = await AccountLinking.linkAccounts(recipeUserId, primaryUserId); + if (response.status === "OK") { + if (response.accountsAlreadyLinked) { + // The input users were already linked + } else { + // The two users are now linked } + let modifiedUser = response.user; + console.log(modifiedUser.loginMethods); // this will now contain the login method of the recipeUserId as well. + } else if (response.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if there already exists another primary user with the same email or phone number + // as the recipeUserId's account. + } else if (response.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // This happens if the input primaryUserId is not actually a primary user ID. + // You can call createPrimaryUserId and call linkAccountsAgain + } else if (response.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // This happens if the input recipe user ID is already linked to another primary user. + // You can call unlink accounts on the recipe user ID and then try linking again. + } } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.accountlinking.asyncio import link_accounts from supertokens_python.types import RecipeUserId @@ -260,7 +239,7 @@ async def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserI print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number - # as the recipeUserId's account. + # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. @@ -271,10 +250,8 @@ async def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserI # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` - - - - + + ```python from supertokens_python.recipe.accountlinking.syncio import link_accounts from supertokens_python.types import RecipeUserId @@ -292,7 +269,7 @@ def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): print(modified_user.login_methods) # this will now contain the login method of the recipeUserId as well. elif response.status == "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR": # This happens if there already exists another primary user with the same email or phone number - # as the recipeUserId's account. + # as the recipeUserId's account. pass elif response.status == "INPUT_USER_IS_NOT_A_PRIMARY_USER": # This happens if the input primaryUserId is not actually a primary user ID. @@ -303,62 +280,58 @@ def link_accounts_helper(primary_user_id: str, recipe_user_id: RecipeUserId): # You can call unlink_accounts on the recipe user ID and then try linking again. pass ``` - - - - - - + + + + ### 4. Unlink accounts If you want to unlink an account from its primary user ID, you can use the following function: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import AccountLinking from "supertokens-node/recipe/accountlinking"; import { RecipeUserId } from "supertokens-node"; async function unlinkAccount(recipeUserId: RecipeUserId) { - let response = await AccountLinking.unlinkAccount(recipeUserId); - if (response.status === "OK") { - if (response.wasLinked) { - // This means that we unlinked the account from its primary user ID - } else { - // This means that the user was never linked in the first place - } - - if (response.wasRecipeUserDeleted) { - // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. - // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens - // won't know which user info to return - the primary user, or the recipe user. - // Note that even though the recipe user is deleted, the session, metadata, roles etc for this - // primary user is still intact, and calling getUserById(primaryUserId) will still return - // the user object with the other login methods. - } else { - // There not exists a user account which is not a primary user, with the recipeUserId = to the - // input recipeUserId. - } + let response = await AccountLinking.unlinkAccount(recipeUserId); + if (response.status === "OK") { + if (response.wasLinked) { + // This means that we unlinked the account from its primary user ID + } else { + // This means that the user was never linked in the first place + } + if (response.wasRecipeUserDeleted) { + // This is true if we call unlinkAccount on the recipe user ID of the primary user ID user. + // We delete this user because if we don't and we call getUserById() on this user's ID, SuperTokens + // won't know which user info to return - the primary user, or the recipe user. + // Note that even though the recipe user is deleted, the session, metadata, roles etc for this + // primary user is still intact, and calling getUserById(primaryUserId) will still return + // the user object with the other login methods. + } else { + // There not exists a user account which is not a primary user, with the recipeUserId = to the + // input recipeUserId. } + } } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - - - - + + + + ```python from supertokens_python.recipe.accountlinking.asyncio import unlink_account from supertokens_python.types import RecipeUserId @@ -381,14 +354,12 @@ async def unlink_account_helper(recipe_user_id: RecipeUserId): # the user object with the other login methods. pass else: - # There now exists a user account which is not a primary user, with the recipe_user_id equal to the + # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` - - - - + + ```python from supertokens_python.recipe.accountlinking.syncio import unlink_account from supertokens_python.types import RecipeUserId @@ -411,52 +382,49 @@ def unlink_account_helper(recipe_user_id: RecipeUserId): # the user object with the other login methods. pass else: - # There now exists a user account which is not a primary user, with the recipe_user_id equal to the + # There now exists a user account which is not a primary user, with the recipe_user_id equal to the # input recipe_user_id. pass ``` - - - - - - + + + + ### 5. Convert a `userId` into a `recipeUserId` If you notice, the input to a lot of the functions above is of type `RecipeUserId`. You can convert a string userID into a `RecipeUserId` in the following way: - - + + +:::note[At the moment this feature is not supported through the Go SDK.] +::: + + + + ```tsx import SuperTokens from "supertokens-node"; async function getAsRecipeUserIdType(userId: string) { - return SuperTokens.convertToRecipeUserId(userId); + return SuperTokens.convertToRecipeUserId(userId); } ``` + + - - - -:::note -At the moment this feature is not supported through the Go SDK. -::: - - - - + + ```python from supertokens_python.types import RecipeUserId user_id = "some_user_id"; recipe_user_id = RecipeUserId(user_id) ``` - - - + + The reason for this type is that it prevents bugs wherein a function expects a recipe user ID (like `createNewSession`, or `updateEmailOrPassword` from email password recipe). However, you might pass in the primary user ID instead. @@ -472,9 +440,9 @@ Our SDK also exposes other helper functions: - `AccountLinking.canLinkAccounts`: Given a `recipeUserId` and a primary user ID, this function returns a status `OK` if the accounts can link, and if not, it returns a different status (indicating why the accounts can't link). Accounts can link if the recipe user ID is not already linked to another primary user, and if the resulting primary user does not have any email / phone number in common with another primary user across all the tenants that it belongs to. -- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. +- `AccountLinking.isSignUpAllowed`: Given the login info (email for example) of the new user, who is trying to sign up, this function returns `true` if it's safe to allow them to sign up, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`. -- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#support-status-codes) to see why this might return `false`. +- `AccountLinking.isSignInAllowed`: Given the login info (email for example) of a user, who is trying to sign in, this function returns `true` if it's safe to allow them to sign in, `false` otherwise. See the [error codes in the automatic account linking page](./automatic-account-linking#error-status-codes) to see why this might return `false`. - `AccountLinking.isEmailChangeAllowed`: Given the recipe user ID and the new email for update, this function returns `true` if it's safe to update the email, else `false`. Below are the conditions in which `false` returns: - If the input recipe user is a primary user, then ensure that the new email doesn't belong to any other primary user. If it does, the change is not allowed since multiple primary users can't have the same email. @@ -484,9 +452,9 @@ Our SDK also exposes other helper functions: ## See also - - - - - - + + + + + + diff --git a/docs/post-authentication/account-linking/meta.ts b/docs/post-authentication/account-linking/meta.ts new file mode 100644 index 0000000000..f6d5f930ca --- /dev/null +++ b/docs/post-authentication/account-linking/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Account Linking", + icon: "file-user", + order: 3, +}); diff --git a/docs/post-authentication/dashboard/_category_.json b/docs/post-authentication/dashboard/_category_.json deleted file mode 100644 index 78534d5630..0000000000 --- a/docs/post-authentication/dashboard/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Dashboard", - "position": 4 -} diff --git a/docs/post-authentication/dashboard/initial-setup.mdx b/docs/post-authentication/dashboard/initial-setup.mdx index a75afbe045..ac043387e9 100644 --- a/docs/post-authentication/dashboard/initial-setup.mdx +++ b/docs/post-authentication/dashboard/initial-setup.mdx @@ -1,36 +1,23 @@ --- title: Initial setup -hide_title: true -sidebar_position: 2 -description: >- - Set up and manage your user management dashboard with SuperTokens integration - and configuration instructions. -page_type: tutorial -recipe: dashboard -category: dashboard +description: Set up and manage your user management dashboard with SuperTokens integration and configuration instructions. +sidebar: + order: 2 --- - -# Setting up the dashboard - ## Overview The following page shows you how to set up the dashboard recipe and access the web interface. You can check the next diagram to understand how the dashboard integrates with your application. - - -Flowchart of architecture when using SuperTokens managed service - - -Flowchart of architecture when self-hosting SuperTokens - + + +Flowchart of architecture when using SuperTokens managed service + + +Flowchart of architecture when self-hosting SuperTokens + @@ -38,13 +25,11 @@ values={[ ### 1. Initialize the `Dashboard` recipe - To get started, initialize the Dashboard recipe in the `recipeList`. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Dashboard from "supertokens-node/recipe/dashboard"; @@ -57,16 +42,12 @@ SuperTokens.init({ }, recipeList: [ // TODO: Initialise other recipes - // highlight-start Dashboard.init(), - // highlight-end ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/dashboard" @@ -77,125 +58,128 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // TODO: Initialise other recipes - // highlight-start dashboard.Init(nil), - // highlight-end }, }); } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ # TODO: Initialise other recipes - # highlight-start dashboard.init(), - # highlight-end ] ) ``` + + - - - -#### Update your content security policy {{optional}} - - - +#### Update your content security policy (optional) + + If your backend returns a `Content-Security-Policy` header, you encounter the following UI displaying the Content Security Policy violation details. Follow the instructions provided in this UI to make necessary adjustments to your backend Content Security Policy configuration. -![Content Security Policy error handled UI](/img/dashboard/csp-error.png) - +![Content Security Policy error handled UI](/docs-assets/img/dashboard/csp-error.png) For example, to address the error message displayed in the above screenshot, you need to modify your `original policy`. In the given example, it appears as follows: + + +If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. + + +If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. + + + + ```text script-src: - 'self' - 'unsafe-inline' + 'self' + 'unsafe-inline' https://google.com img-src: https://google.com ``` - -To resolve this issue, make the following adjustments: - - + + ```text script-src: - 'self' - 'unsafe-inline' - https://google.com - + 'self' + 'unsafe-inline' + https://cdn.jsdelivr.net/gh/supertokens/ img-src: - https://google.com https://cdn.jsdelivr.net/gh/supertokens/ - + https://purecatamphetamine.github.io/ ``` -Essentially, you need to include the domain listed as the `Blocked URI` in your violated directive block within your original policy. - - - -If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. - + + ```text script-src: - 'self' - 'unsafe-inline' + 'self' + 'unsafe-inline' https://cdn.jsdelivr.net/gh/supertokens/ img-src: https://cdn.jsdelivr.net/gh/supertokens/ https://purecatamphetamine.github.io/ ``` + + - - - -If you return a `Content-Security-Policy` header from your backend, you need to include the following directives for the user management dashboard to work correctly. + + +To resolve this issue, make the following adjustments: + + + + ```text script-src: - 'self' - 'unsafe-inline' - https://cdn.jsdelivr.net/gh/supertokens/ + 'self' + 'unsafe-inline' + https://google.com + img-src: + https://google.com https://cdn.jsdelivr.net/gh/supertokens/ - https://purecatamphetamine.github.io/ + ``` + + - - + + +Essentially, you need to include the domain listed as the `Blocked URI` in your violated directive block within your original policy. + + ### 2. Access the dashboard -:::important -The backend SDK serves the user management dashboard, and you have to use your API domain when trying to visit the dashboard. +:::note[The backend SDK serves the user management dashboard, and you have to use your API domain when trying to visit the dashboard.] ::: -Navigate to `^{appInfo.apiDomain}^{appInfo.apiBasePath}/dashboard` to view the dashboard. +Navigate to `/auth/dashboard` to view the dashboard. -:::note -If you are using Next.js, upon integrating the backend SDK into your Next.js API folder, the dashboard becomes accessible by default at `^{appInfo.apiDomain}/api/auth/dashboard`. For frameworks other than Next.js, access it at `^{appInfo.apiDomain}/auth/dashboard`. Should you have customized the `apiBasePath` configuration property, navigate to `^{appInfo.apiDomain}^{appInfo.apiBasePath}/dashboard` to access the dashboard. +:::note[If you are using Next.js, upon integrating the backend SDK into your Next.js API folder, the dashboard becomes accessible by default at `/api/auth/dashboard`. For frameworks other than Next.js, access it at `/auth/dashboard`. Should you have customized the `apiBasePath` configuration property, navigate to `/auth/dashboard` to access the dashboard.] ::: -Dashboard login screen UI +Dashboard login screen UI ### 3. Create dashboard credentials -:::info Paid Feature -You can create 3 dashboard users* for free. +:::info[Paid Feature] +You can create 3 dashboard users* for free. If you need to create additional users: @@ -207,16 +191,15 @@ If you need to create additional users: When you first set up SuperTokens, there are no credentials created for the dashboard. If you click the "Add a new user" button in the dashboard login screen you can see the command you need to execute to create credentials. -Dashboard sign up screen UI +Dashboard sign up screen UI -To create credentials you need to make a request to SuperTokens core. +To create credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. - Replace `` and `` with the appropriate values. -:::caution -If using self-hosted SuperTokens core, you need to make sure that you add an API key to the core in case it's exposed to the internet. Otherwise, anyone can create or modify dashboard users. +:::warning[If using self-hosted SuperTokens core, you need to make sure that you add an API key to the core in case it's exposed to the internet. Otherwise, anyone can create or modify dashboard users.] You can add an API key to the core by following the instructions "Auth flow customizations" > "SuperTokens core settings" > "Adding API keys" page. ::: @@ -225,9 +208,9 @@ You can add an API key to the core by following the instructions "Auth flow cust You can update the email or password of existing credentials by using the "Forgot Password" button on the dashboard login page. -Reset your password screen UI +Reset your password screen UI -To update credentials you need to make a request to SuperTokens core. +To update credentials you need to make a request to SuperTokens core. - The example above uses the demo core `https://try.supertokens.com`, replace this with the connection URI you pass to the backend SDK when initialising SuperTokens. - Replace `` with your API key. If you are using a self hosted SuperTokens core there is no API key by default. In that case you can either skip or ignore the `api-key` header. @@ -239,14 +222,12 @@ When using the dashboard recipe, you can restrict access to certain features by You can provide an array of emails to the backend SDK when initialising the dashboard recipe: -:::important -- Not providing an admins array results in all dashboard users having both read and write operations. +:::note[- Not providing an admins array results in all dashboard users having both read and write operations.] - Providing an empty array as admins results in all dashboard users having only read access. ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Dashboard from "supertokens-node/recipe/dashboard"; @@ -259,20 +240,14 @@ SuperTokens.init({ }, recipeList: [ // TODO: Initialise other recipes - // highlight-start Dashboard.init({ - admins: [ - "johndoe@gmail.com", - ], + admins: ["johndoe@gmail.com"], }), - // highlight-end ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/dashboard" @@ -284,41 +259,34 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // TODO: Initialise other recipes - // highlight-start dashboard.Init(&dashboardmodels.TypeInput{ Admins: &[]string{ "johndoe@gmail.com", }, }), - // highlight-end }, }); } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import dashboard init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ # TODO: Initialise other recipes - # highlight-start dashboard.init( admins=[ "johndoe@gmail.com", ], ), - # highlight-end ] ) ``` - - - + + diff --git a/docs/post-authentication/dashboard/introduction.mdx b/docs/post-authentication/dashboard/introduction.mdx index 5274512f41..1084e231ac 100644 --- a/docs/post-authentication/dashboard/introduction.mdx +++ b/docs/post-authentication/dashboard/introduction.mdx @@ -1,17 +1,10 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true description: 'Learn how to manage users in SuperTokens ' -page_type: overview -recipe: dashboard -category: dashboard +sidebar: + order: 1 --- - -# Dashboard - ## Overview With the user management dashboard, you can perform actions through a user interface. @@ -22,32 +15,16 @@ This allows you to view and modify tenants and users through different web actio To get a quick understanding of how the dashboard looks and what it can do you can visit the [live demo](https://dashboard.demo.supertokens.com/api/auth/dashboard). The credentials for logging in are: `email: demo@supertokens.com` and `password: abcd1234`. -Alternatively, you can follow the [quickstart guide](/docs/post-authentication/dashboard/initial-setup) to configure the dashboard in your **SuperTokens** integration. - - - - - Initial setup - - - See how you can initialize the dashboard in your application. - - - - - User management - - - Work with users through the dashboard. - - - - - Tenant management - - - Work with tenants through the dashboard. - - - - +Alternatively, you can follow the [quickstart guide](/post-authentication/dashboard/initial-setup) to configure the dashboard in your **SuperTokens** integration. + + + +See how you can initialize the dashboard in your application. + + +Work with users through the dashboard. + + +Work with tenants through the dashboard. + + diff --git a/docs/post-authentication/dashboard/meta.ts b/docs/post-authentication/dashboard/meta.ts new file mode 100644 index 0000000000..1f1524a6a2 --- /dev/null +++ b/docs/post-authentication/dashboard/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Dashboard", + icon: "layout-dashboard", + order: 4, +}); diff --git a/docs/post-authentication/dashboard/tenant-management.mdx b/docs/post-authentication/dashboard/tenant-management.mdx index 8894639e7c..da9fe19656 100644 --- a/docs/post-authentication/dashboard/tenant-management.mdx +++ b/docs/post-authentication/dashboard/tenant-management.mdx @@ -1,29 +1,21 @@ --- title: Tenant Management -hide_title: true -sidebar_position: 4 -toc_max_heading_level: 4 -description: >- - Manage tenants, login methods, and multi-factor authentication using the - tenant management dashboard. -page_type: guide -recipe: dashboard -category: dashboard +description: Manage tenants, login methods, and multi-factor authentication using the tenant management dashboard. +sidebar: + order: 4 --- -# Tenant management - ## Overview This page shows you what actions you can perform on tenants through the dashboard. -:::info Caution +:::info[Caution] This is only available with Node and Python SDKs. ::: -Tenant Management Landing +Tenant Management Landing --- @@ -31,20 +23,20 @@ This is only available with Node and Python SDKs. Clicking on `Add Tenant` prompts you to enter the tenant id. Once you enter the tenant id, click on `Create Now` to create the tenant. You then proceed to the Tenant Details page where you can further manage the newly created tenant. -Create Tenant +Create Tenant ## View tenant details Upon selection or creation of a tenant, the Tenant Details page appears. The sections appear below. -Tenant details +Tenant details ### Tenant ID and users -The first section shows the tenant ID and the number of users in that tenant. Clicking on `See Users` takes you to the [user management page](/docs/post-authentication/dashboard/user-management) where you can view and manage the users for the selected tenant. +The first section shows the tenant ID and the number of users in that tenant. Clicking on `See Users` takes you to the [user management page](/post-authentication/dashboard/user-management) where you can view and manage the users for the selected tenant. -Tenant users +Tenant users ### Enabled login methods @@ -57,36 +49,34 @@ Appropriate recipes must be active to turn on the login methods. For example, :::info -If you are using the Auth React SDK, make sure to enable [usesDynamicLoginMethods](/docs/authentication/enterprise/common-domain-login#3-tell-supertokens-about-the-saved-tenantid-from-the-previous-step) to ensure the frontend automatically shows the login methods based on the selection here. +If you are using the Auth React SDK, make sure to enable `usesDynamicLoginMethods` in your tenant configuration to ensure the frontend automatically shows the login methods based on the selection here. See [tenant configuration](/authentication/enterprise/manage-tenants) for details. ::: -Login Methods +Login Methods ### Secondary factors -This section displays the secondary factors available for the tenant. By enabling these toggles, the corresponding factor becomes active for all users of the tenant. Refer to [MultiFactor Authentication docs](/docs/additional-verification/mfa/introduction) for more information. +This section displays the secondary factors available for the tenant. By enabling these toggles, the corresponding factor becomes active for all users of the tenant. Refer to [MultiFactor Authentication docs](/additional-verification/mfa/introduction) for more information. -[MultiFactorAuth](/docs/additional-verification/mfa/initial-setup) recipe must initialize to enable Secondary Factors. +[MultiFactorAuth](/additional-verification/mfa/initial-setup) recipe must initialize to enable Secondary Factors. Also, initialize appropriate recipes in the backend SDK to use a secondary factor. For example, - to turn on TOTP, initialize the TOTP recipe in the backend. - to turn on `OTP Phone`, initialize the Passwordless recipe with `flowType` `USER_INPUT_CODE` and contactMethod `PHONE` -Secondary Factors +Secondary Factors ### Core configuration -Core Configuration +Core Configuration This section shows the current configuration values in core for the tenant. You can edit some of these settings by clicking the `pencil` icon next to the property. -Edit Core Configuration - -:::caution +Edit Core Configuration -Some configuration values may not be editable since they inherit from the App. If using SuperTokens managed hosting, you can modify deployment-level values on the **Configuration** page in the SaaS Dashboard. Else, if you are self-hosting the SuperTokens core, edit them via Docker environment variables or the `configuration.yaml` file. +:::warning[Some configuration values may not be editable since they inherit from the App. If using SuperTokens managed hosting, you can modify deployment-level values on the **Configuration** page in the SaaS Dashboard. Else, if you are self-hosting the SuperTokens core, edit them via Docker environment variables or the `configuration.yaml` file.] ::: @@ -98,27 +88,27 @@ The Social/Enterprise providers section becomes available once `Third Party` log Initially, configure a new provider. -Add provider prompt +Add provider prompt Later on, you can configure new or existing third-party providers from the **Social/Enterprise providers** section. -Social/Enterprise providers +Social/Enterprise providers ### Configure a new provider When adding a new third-party provider, you receive a list of available options, including built-in enterprise and social providers, custom, and SAML. -New Provider +New Provider Upon selection of the desired provider, provide further details such as `Client ID`, `Client Secret`, etc. -New Provider Details +New Provider Details #### Enterprise providers For the Enterprise providers, provide certain extra information before proceeding to the Provider details. For example, Active Directory provider requires a `Directory ID` before editing further details. -Additional configuration for Active Directory +Additional configuration for Active Directory #### Custom providers @@ -126,32 +116,28 @@ If a Social/Enterprise provider is not available in the list of built-in provide Start off by providing `ThirdParty ID`, `Name` and Client details such as `Client ID`, `Secret`, `Scope`, etc. -Custom Provider basic details +Custom Provider basic details If using an OpenID compliant provider, you could add the `OIDC Discovery Endpoint`. Otherwise, configure the provider by manually providing `Authorization Endpoint`, `Token Endpoint`, `User Info Endpoint`, etc. -OpenID configuration +OpenID configuration Finally, clicking on `Save` adds the Social/enterprise provider for the tenant. #### SAML providers -To add a SAML provider, use the `Add SAML Provider` option. For more information on what is SAML and how it works with SuperTokens, refer [SAML docs](/docs/authentication/enterprise/saml). +To add a SAML provider, use the `Add SAML Provider` option. For more information on what is SAML and how it works with SuperTokens, refer [SAML docs](/authentication/enterprise/saml). Upon selection, provide the `Boxy URL` and the `Boxy API Key`. -:::important - -To use SAML providers, an additional Boxy HQ service is necessary. You can either self-host yourself or email for a managed instance. Details for them are also available on this page. +:::note[To use SAML providers, an additional Boxy HQ service is necessary. You can either self-host yourself or email for a managed instance. Details for them are also available on this page.] ::: -Boxy SAML Prompt +Boxy SAML Prompt On continuing, you are further asked for the SAML configuration. You have an option to either provide SAML XML directly or via the Metadata URL from the Provider. Also, fill in other details such as `Suffix`, `Name`, `Redirect URLs` and click on `Save` to add the SAML provider. -:::caution - -Adding ThirdParty suffix is not compulsory, however if you wish to add multiple SAML providers for a tenant, you need to add unique suffixes for each of them. +:::warning[Adding ThirdParty suffix is not compulsory, however if you wish to add multiple SAML providers for a tenant, you need to add unique suffixes for each of them.] ::: diff --git a/docs/post-authentication/dashboard/user-management.mdx b/docs/post-authentication/dashboard/user-management.mdx index 7ab1b33afd..7e0fcfc9f3 100644 --- a/docs/post-authentication/dashboard/user-management.mdx +++ b/docs/post-authentication/dashboard/user-management.mdx @@ -1,17 +1,10 @@ --- title: User management -hide_title: true -sidebar_position: 3 -description: >- - View and manage user details, edit information, and perform actions in the - user dashboard. -page_type: guide -recipe: dashboard -category: dashboard +description: View and manage user details, edit information, and perform actions in the user dashboard. +sidebar: + order: 3 --- -# User management - ## Overview With the user management dashboard you can view the list of users and their details. @@ -23,13 +16,13 @@ You can also perform different operations on these users as mentioned below. If you have created your app, you may not have any users to show on the dashboard. -Empty dashboard screen UI +Empty dashboard screen UI ## List users Navigate to your frontend app and create a user (via the sign-up flow). On creation, if you head back to the dashboard and refresh the page, you see that user: -One user in dashboard screen UI +One user in dashboard screen UI --- @@ -37,9 +30,9 @@ Navigate to your frontend app and create a user (via the sign-up flow). On creat When you select a user you can view detailed information about the user such as email, phone number, user metadata, etc. -User details page screen UI part one +User details page screen UI part one -User details page screen UI part two +User details page screen UI part two --- @@ -47,9 +40,9 @@ When you select a user you can view detailed information about the user such as You can edit user information and perform actions such as resetting a user's password or revoking sessions for a user. -Change password modal UI +Change password modal UI -:::info Note +:::info[Note] Enable some features such as user metadata and email verification in your backend before you can use them in the user management dashboard. ::: @@ -57,13 +50,13 @@ Enable some features such as user metadata and email verification in your backen ## Create user roles and permissions -:::caution no-title +:::warning This feature is only available through the Node.js SDK. ::: When you first use the `UserRoles` recipe, the list of roles is empty. To create roles, click on the "Add Role" button. -No roles created +No roles created This action opens a modal, enabling you to create a role along with its associated permissions. Permissions are essentially a list of strings assigned to a specific role. @@ -71,25 +64,25 @@ This action opens a modal, enabling you to create a role along with its associat ## List user roles -:::caution no-title +:::warning This feature is only available through the Node.js SDK. ::: -Create role +Create role After creating a role, the UI should display a list of all roles in your app. -Roles list +Roles list You can preview the role you created by clicking on the role row. The modal provides options to edit or delete the role. -Preview role +Preview role --- ## Assign user roles -:::caution no-title +:::warning This feature is only available through the Node.js SDK. ::: @@ -97,21 +90,21 @@ To assign a specific role to a user, start by finding the user in the dashboard. If the selected user has associations with multiple tenants, you can choose a `tenantId` from the dropdown menu to specify the tenant for which you'd like to assign roles. -Select tenant +Select tenant Click the edit button to start assigning roles. Then, select the "Assign Role" button, and a modal appears with a list of available roles for assignment to this user. -Assign role +Assign role --- ## Remove user roles -:::caution no-title +:::warning This feature is only available through the Node.js SDK. ::: To remove a role assigned to a user, click on the "X" icon next to that specific role. -View assigned role +View assigned role diff --git a/docs/post-authentication/meta.ts b/docs/post-authentication/meta.ts new file mode 100644 index 0000000000..2812958060 --- /dev/null +++ b/docs/post-authentication/meta.ts @@ -0,0 +1,8 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Post Authentication", + icon: "users", + order: 60, + collapsed: true, +}); diff --git a/docs/post-authentication/post-login-redirect.mdx b/docs/post-authentication/post-login-redirect.mdx index cd8a09ebcf..c75107fe29 100644 --- a/docs/post-authentication/post-login-redirect.mdx +++ b/docs/post-authentication/post-login-redirect.mdx @@ -1,94 +1,85 @@ --- title: Post Login Redirect -hide_title: true -sidebar_position: 1 -description: >- - Change post-login redirection paths and configure login screen behavior using - SuperTokens. -page_type: guide -category: post-authentication +description: Change post-login redirection paths and configure login screen behavior using SuperTokens. +sidebar: + order: 1 --- - -# Post login redirection - ## Change redirection path post login - - - + + By default, the user is redirected to the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: + + +By default, the user is redirected the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: + + + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.createdNewUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - }, - // highlight-end - recipeList: [ /* Recipe list */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + recipeList: [ + /* Recipe list */ + ], }); ``` - - - - - -By default, the user is redirected the the `/` route on your website post login. To change this, you can use the `getRedirectionURL` function on the frontend as shown below: - -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - if (context.redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return context.redirectToPath; - } - if (context.createdNewUser) { - // user signed up - } else { - // user signed in - } - return "/dashboard"; - } - return undefined; - }, - // highlight-end - recipeList: [ /* Recipe list */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + if (context.redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return context.redirectToPath; + } + if (context.createdNewUser) { + // user signed up + } else { + // user signed in + } + return "/dashboard"; + } + return undefined; + }, + recipeList: [ + /* Recipe list */ + ], }); ``` - - - - + + The user will be redirected to the provided URL on: - Successful sign up. @@ -99,160 +90,152 @@ The user will be redirected to the provided URL on: If you want to redirect the user to a different domain, then you can first redirect them to a specific path using the function above, which further redirects them to the final domain. :::info -Please refer to [this page](/docs/references/frontend-sdks/hooks#redirection-callback-hook) to learn more about the `getRedirectionURL` hook. +Please refer to [this page](/references/frontend-sdks/hooks#redirection-callback-hook) to learn more about the `getRedirectionURL` hook. ::: ## Redirect user to the login page - - - + + Use the `redirectToAuth({show?: "signin" | "signup", redirectBack?: boolean}?)` function to redirect the user to the login screen. For example, you may want to call this function when the user clicks on the login button. + + +Redirect the user to the `/auth` (this is the default path for the pre-built UI) + + +Redirect the user to the `/auth` (this is the default path for the pre-built UI) + + + + ```tsx -// highlight-next-line import React from "react"; import { redirectToAuth } from "supertokens-auth-react"; -function NavBar () { - async function onLogin () { - // highlight-next-line - redirectToAuth(); +function NavBar() { + async function onLogin() { + redirectToAuth(); } return (
    -
  • Home
  • - // highlight-next-line -
  • Login
  • - +
  • Home
  • +
  • Login
- ) + ); } ``` - -- Call `redirectToAuth({show: "signin"})` to take them to the sign in screen -- Call `redirectToAuth({show: "signup"})` to take them to the sign up screen -- If you do not want the user to be redirected to the current page post sign in, use `redirectToAuth({redirectBack: false})` - -
- - -Redirect the user to the `/auth` (this is the default path for the pre-built UI) - + + ```ts import { Component } from "@angular/core"; @Component({ - selector: 'nav-bar', - template: ` -
    -
  • Home
  • - // highlight-next-line -
  • Login
  • -
- `, + selector: "nav-bar", + template: ` +
    +
  • Home
  • +
  • Login
  • +
+ `, }) export class NavBarComponent { - async onLogin () { - // highlight-next-line - window.location.href = "/auth?show=signin&redirectToPath=" + encodeURIComponent(window.location.pathname); - } + async onLogin() { + window.location.href = "/auth?show=signin&redirectToPath=" + encodeURIComponent(window.location.pathname); + } } ``` - -- Set `show=signin` to take them to the sign in screen -- Set `show=signup` to take them to the sign up screen -- Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one). - -
- - - -Redirect the user to the `/auth` (this is the default path for the pre-built UI) - + + ```html ``` + + + + +- Call `redirectToAuth({show: "signin"})` to take them to the sign in screen +- Call `redirectToAuth({show: "signup"})` to take them to the sign up screen +- If you do not want the user to be redirected to the current page post sign in, use `redirectToAuth({redirectBack: false})` + + - Set `show=signin` to take them to the sign in screen - Set `show=signup` to take them to the sign up screen - Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one). - - -
+ + +- Set `show=signin` to take them to the sign in screen +- Set `show=signup` to take them to the sign up screen +- Set `redirectToPath` to redirect the user to a specific page after they have signed in, or you can skip it to take them to the `/` route (which is the default one). + + ## Showing sign up by default The login screen shows the sign in UI by default, to change that, you can set the following config: - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-next-line - defaultToSignUp: true, - recipeList: [ /* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + defaultToSignUp: true, + recipeList: [ + /* ... */ + ], }); ``` - - - - - -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-next-line - defaultToSignUp: true, - recipeList: [ /* ... */] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + defaultToSignUp: true, + recipeList: [ + /* ... */ + ], }); ``` - - - + + --- ## See also - - - - - - - + + + + + + + diff --git a/docs/post-authentication/session-management/_category_.json b/docs/post-authentication/session-management/_category_.json deleted file mode 100644 index 2e192d6606..0000000000 --- a/docs/post-authentication/session-management/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Session Management", - "position": 1.1 -} diff --git a/docs/post-authentication/session-management/access-session-data.mdx b/docs/post-authentication/session-management/access-session-data.mdx index 4b92890eac..2d6fd132db 100644 --- a/docs/post-authentication/session-management/access-session-data.mdx +++ b/docs/post-authentication/session-management/access-session-data.mdx @@ -1,20 +1,12 @@ --- title: Access Session Data -hide_title: true -sidebar_position: 2 -toc_max_heading_level: 4 description: >- - Learn how to access session data including JWT tokens, tenant IDs, and user - sessions across different programming languages and frameworks. -page_type: guide -recipe: session -category: session-management + Learn how to access session data including JWT tokens, tenant IDs, and user sessions across different programming + languages and frameworks. +sidebar: + order: 20 --- - - -# Access session data - ## Overview The session data is accessible, both in the backend and on the frontend, after a user has successfully logged in. @@ -23,7 +15,9 @@ This guide shows you how to access different session properties. ## Before you start - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: --- @@ -32,11 +26,20 @@ This guide shows you how to access different session properties. ### On the backend - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -44,19 +47,15 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express let app = express(); app.get("/getJWT", verifySession(), async (req, res) => { + let session = req.session; - let session = req.session; + let jwt = session.getAccessToken(); - // highlight-start - let jwt = session.getAccessToken(); - // highlight-end - - res.json({ token: jwt }) + res.json({ token: jwt }); }); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -65,72 +64,65 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/getJWT", - method: "get", - options: { - pre: [ - { - method: verifySession() - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let session = req.session; + path: "/getJWT", + method: "get", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let session = req.session; - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end - return res.response({ token: jwt }).code(200); - } -}) + let jwt = session!.getAccessToken(); + return res.response({ token: jwt }).code(200); + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; let fastify = Fastify(); -fastify.get("/getJWT", { +fastify.get( + "/getJWT", + { preHandler: verifySession(), -}, (req, res) => { + }, + (req, res) => { let session = req.session; - // highlight-start let jwt = session.getAccessToken(); - // highlight-end res.send({ token: jwt }); -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function getJWT(awsEvent: SessionEvent) { - let session = awsEvent.session; + let session = awsEvent.session; - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end + let jwt = session!.getAccessToken(); - return { - body: JSON.stringify({ token: jwt }), - statusCode: 200, - }; -}; + return { + body: JSON.stringify({ token: jwt }), + statusCode: 200, + }; +} exports.handler = verifySession(getJWT); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -139,18 +131,14 @@ import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); router.get("/getJWT", verifySession(), (ctx: SessionContext, next) => { - let session = ctx.session; + let session = ctx.session; - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end - ctx.body = { token: jwt }; + let jwt = session!.getAccessToken(); + ctx.body = { token: jwt }; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, get, response } from "@loopback/rest"; @@ -158,106 +146,65 @@ import { verifySession } from "supertokens-node/recipe/session/framework/loopbac import { SessionContext } from "supertokens-node/framework/loopback"; class GetJWT { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) { } - @get("/getJWT") - @intercept(verifySession()) - @response(200) - handler() { - let session = this.ctx.session; - - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end - return { token: jwt }; - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} + @get("/getJWT") + @intercept(verifySession()) + @response(200) + handler() { + let session = this.ctx.session; + + let jwt = session!.getAccessToken(); + return { token: jwt }; + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function getJWT(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) - let session = req.session; - - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end - res.json({ token: jwt }) -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); + let session = req.session; -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // highlight-start - let jwt = session!.getAccessToken(); - // highlight-end - return NextResponse.json({ token: jwt }); - }); + let jwt = session!.getAccessToken(); + res.json({ token: jwt }); } ``` - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Get, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { - @Get('example') + @Get("example") @UseGuards(new AuthGuard()) async postExample(@Session() session: SessionContainer): Promise<{ token: any }> { - //highlight-start // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. const jwt = session.getAccessToken(); - //highlight-end return { token: jwt }; } } ``` - - - - - + + + + ```go import ( "fmt" @@ -271,60 +218,49 @@ func getJWT(w http.ResponseWriter, r *http.Request) { // retrieve the session object as shown below sessionContainer := session.GetSessionFromRequestContext(r.Context()) - // highlight-start jwt := sessionContainer.GetAccessToken() - // highlight-end fmt.Println(jwt) } ``` - - - - - - - -```python + + + + +```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session -@app.get('/getJWT') # type: ignore +@app.get('/getJWT') async def get_jwt(session: SessionContainer = Depends(verify_session())): - # highlight-next-line current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session -@app.route('/getJWT', methods=['GET']) # type: ignore +@app.route('/getJWT', methods=['GET']) @verify_session() def get_jwt(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens - # highlight-next-line current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` - - - - -```python + + +```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest @@ -335,19 +271,47 @@ from supertokens_python.recipe.session.framework.django.asyncio import verify_se @verify_session() async def get_jwt(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, request.supertokens) - # highlight-next-line current_jwt = session.get_access_token() print(current_jwt) # TODO... ``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let jwt = session!.getAccessToken(); + return NextResponse.json({ token: jwt }); + }); +} +``` - - + + + + + ### On the frontend @@ -356,38 +320,33 @@ async def get_jwt(request: HttpRequest): When using cookie based auth, by default, the access token is not readable by the SDK on the frontend (since it's stored as `httpOnly` cookie). To enable this, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` parameter to `true`. -:::important -If you are only using header-based sessions, you can skip this step +:::note[If you are only using header-based sessions, you can skip this step] ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - exposeAccessTokenToFrontendInCookieBasedAuth: true, - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + exposeAccessTokenToFrontendInCookieBasedAuth: true, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -399,133 +358,113 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ExposeAccessTokenToFrontendInCookieBasedAuth: true, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import session init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-next-line expose_access_token_to_frontend_in_cookie_based_auth=True ) ] ) ``` + + - - +#### 2. Read the access token -#### 2. Read the access token + - - - - - - + + + ```tsx -import Session from 'supertokens-auth-react/recipe/session'; +import Session from "supertokens-auth-react/recipe/session"; async function getJWT() { - if (await Session.doesSessionExist()) { - let userId = await Session.getUserId(); - let jwt = await Session.getAccessToken(); - } + if (await Session.doesSessionExist()) { + let userId = await Session.getUserId(); + let jwt = await Session.getAccessToken(); + } } ``` - - - - + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function getJWT() { - if (await Session.doesSessionExist()) { - let userId = await Session.getUserId(); - let jwt = await Session.getAccessToken(); - } + if (await Session.doesSessionExist()) { + let userId = await Session.getUserId(); + let jwt = await Session.getAccessToken(); + } } ``` + + - - + - + - - - - - - + + + + ```tsx -import Session from 'supertokens-web-js/recipe/session'; +import Session from "supertokens-web-js/recipe/session"; async function getJWT() { - if (await Session.doesSessionExist()) { - let userId = await Session.getUserId(); - let jwt = await Session.getAccessToken(); - } + if (await Session.doesSessionExist()) { + let userId = await Session.getUserId(); + let jwt = await Session.getAccessToken(); + } } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="Requires SDK globals from surrounding application" async function getJWT() { - if (await supertokensSession.doesSessionExist()) { - let userId = await supertokensSession.getUserId(); - let jwt = await supertokensSession.getAccessToken(); - } + if (await supertokensSession.doesSessionExist()) { + let userId = await supertokensSession.getUserId(); + let jwt = await supertokensSession.getAccessToken(); + } } ``` - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; async function getJWT() { - if (await SuperTokens.doesSessionExist()) { - let userId = await SuperTokens.getUserId(); - let jwt = await SuperTokens.getAccessToken(); - } + if (await SuperTokens.doesSessionExist()) { + let userId = await SuperTokens.getUserId(); + let jwt = await SuperTokens.getAccessToken(); + } } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -537,11 +476,8 @@ class MainApplication: Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -553,11 +489,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -569,21 +502,19 @@ Future getJWT() async { } } ``` + + + + - - - - - - - + --- ## Access the Tenant ID -:::info Multi Tenancy +:::info[Multi Tenancy] This feature is only relevant if you are using the multi tenancy feature. ::: @@ -591,11 +522,20 @@ The session's access token payload contains the tenant ID in the `tId` claim. Yo ### On the backend - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -603,17 +543,13 @@ import { SessionRequest } from "supertokens-node/framework/express"; let app = express(); -// highlight-start app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { - let tenantId = req.session!.getTenantId(); - // highlight-end - //.... + let tenantId = req.session!.getTenantId(); + //.... }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -622,26 +558,23 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - //highlight-start - options: { - pre: [ - { - method: verifySession() - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let tenantId = req.session!.getTenantId(); - //highlight-end - //... - } -}) + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let tenantId = req.session!.getTenantId(); + //... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -649,35 +582,32 @@ import { SessionRequest } from "supertokens-node/framework/fastify"; let fastify = Fastify(); -//highlight-start -fastify.post("/like-comment", { +fastify.post( + "/like-comment", + { preHandler: verifySession(), -}, (req: SessionRequest, res) => { + }, + (req: SessionRequest, res) => { let tenantId = req.session!.getTenantId(); - //highlight-end //.... -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { - let tenantId = awsEvent.session!.getTenantId(); - //.... -}; + let tenantId = awsEvent.session!.getTenantId(); + //.... +} -//highlight-next-line exports.handler = verifySession(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -685,17 +615,13 @@ import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); -//highlight-start router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { - let tenantId = ctx.session!.getTenantId(); - //highlight-end - //.... + let tenantId = ctx.session!.getTenantId(); + //.... }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -703,104 +629,65 @@ import { verifySession } from "supertokens-node/recipe/session/framework/loopbac import { SessionContext } from "supertokens-node/framework/loopback"; class LikeComment { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @intercept(verifySession()) - @response(200) - handler() { - let tenantId = (this.ctx as SessionContext).session!.getTenantId(); - //highlight-end - //.... - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession()) + @response(200) + handler() { + let tenantId = (this.ctx as SessionContext).session!.getTenantId(); + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function likeComment(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) - - let tenantId = req.session!.getTenantId(); - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let tenantId = session!.getTenantId(); - //.... - return NextResponse.json({}) - }); + let tenantId = req.session!.getTenantId(); + //.... } ``` - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { - //highlight-start let tenantId = session.getTenantId(); - //highlight-end //.... return true; } } ``` - - - - - - - + + + + + + ```go import ( "fmt" @@ -825,10 +712,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(tenantID) } ``` - - - - + + ```go import ( "fmt" @@ -868,10 +753,8 @@ func likeCommentAPI(c *gin.Context) { fmt.Println(tenantID) } ``` - - - - + + ```go import ( "fmt" @@ -897,10 +780,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(tenantID) } ``` - - - - + + ```go import ( "fmt" @@ -926,55 +807,46 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(tenantID) } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session -@app.get('/getTenantId') # type: ignore +@app.get('/getTenantId') async def get_tenant_id(session: SessionContainer = Depends(verify_session())): - # highlight-next-line tenant_id = session.get_tenant_id() print(tenant_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session -@app.route('/getTenantId', methods=['GET']) # type: ignore +@app.route('/getTenantId', methods=['GET']) @verify_session() def get_tenant_id(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens - # highlight-next-line tenant_id = session.get_tenant_id() print(tenant_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest @@ -985,27 +857,55 @@ from supertokens_python.recipe.session.framework.django.asyncio import verify_se @verify_session() async def get_tenant_id(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, request.supertokens) - # highlight-next-line tenant_id = session.get_tenant_id() print(tenant_id) ``` + + + + + + + + + + - - +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let tenantId = session!.getTenantId(); + //.... + return NextResponse.json({}); + }); +} +``` -:::note -If you are not using the backend SDK and are doing JWT verification yourself, you can fetch the tenant ID from the JWT by reading the `tId` claim. + + + + + + +:::note[If you are not using the backend SDK and are doing JWT verification yourself, you can fetch the tenant ID from the JWT by reading the `tId` claim.] ::: ### On the frontend -You can read the tenant ID on the frontend by adding the `tId` claim from the [access token payload](/docs/additional-verification/session-verification/claim-validation#using-the-access-token-payload). +You can read the tenant ID on the frontend by adding the `tId` claim from the [access token payload](/additional-verification/session-verification/claim-validation#using-the-access-token-payload). --- @@ -1013,32 +913,28 @@ You can read the tenant ID on the frontend by adding the `tId` claim from the [a Given a user ID, you can fetch all sessions that are active for that user in the following way: - - - + + ```tsx import Session from "supertokens-node/recipe/session"; async function getSessions() { - let userId = "someUserId" // fetch somehow - - // sessionHandles is string[] - // highlight-next-line - let sessionHandles = await Session.getAllSessionHandlesForUser(userId); - - sessionHandles.forEach((handle) => { - - /* we can do the following with the handle: - * - revoke this session - * - change access token payload or session data - * - fetch access token payload or session data - */ - }) + let userId = "someUserId"; // fetch somehow + + // sessionHandles is string[] + let sessionHandles = await Session.getAllSessionHandlesForUser(userId); + + sessionHandles.forEach((handle) => { + /* we can do the following with the handle: + * - revoke this session + * - change access token payload or session data + * - fetch access token payload or session data + */ + }); } ``` - - - + + ```go import ( "fmt" @@ -1048,7 +944,6 @@ import ( func main() { // sessionHandles is string[] - // highlight-next-line tenantId := "public" sessionHandles, err := session.GetAllSessionHandlesForUser("someUserId", &tenantId) if err != nil { @@ -1067,18 +962,16 @@ func main() { } } ``` - - - - - + + + + ```python from supertokens_python.recipe.session.asyncio import get_all_session_handles_for_user async def some_func(): # session_handles is List[string] - # highlight-next-line session_handles = await get_all_session_handles_for_user("someUserId") for _ in session_handles: @@ -1090,15 +983,12 @@ async def some_func(): # - fetch JWT payload or session data # ``` - - - - + + ```python from supertokens_python.recipe.session.syncio import get_all_session_handles_for_user # session_handles is List[string] -# highlight-next-line session_handles = get_all_session_handles_for_user("someUserId") for session_handle in session_handles: @@ -1110,13 +1000,12 @@ for session_handle in session_handles: # - fetch JWT payload or session data # ``` + + + + - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] By default, the method returns all the `session handles` for the user across all the tenants. If you want to fetch the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. @@ -1127,11 +1016,10 @@ If you want to fetch the sessions for a user in a specific tenant, you can pass ## See also - - - - - - - - + + + + + + + diff --git a/docs/post-authentication/session-management/advanced-workflows/_category_.json b/docs/post-authentication/session-management/advanced-workflows/_category_.json deleted file mode 100644 index 8d19e0a1b6..0000000000 --- a/docs/post-authentication/session-management/advanced-workflows/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Advanced Workflows", - "position": 5 -} diff --git a/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting.mdx b/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting.mdx index e5fe2711f5..a940ae7d8e 100644 --- a/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting.mdx @@ -1,45 +1,46 @@ --- title: Blacklist access tokens -hide_title: true -sidebar_position: 7 -description: >- - Enable immediate session revocation by implementing access token blacklisting - with database checks. -page_type: guide -recipe: session -category: session-management +description: Enable immediate session revocation by implementing access token blacklisting with database checks. +sidebar: + order: 70 --- - -# Blacklist access tokens - ## Overview By default, session verification is stateless. This means that SuperTokens does not check that the session actually exists in the database, and only verifies the session by checking its signature. -Whilst this makes session verifications fast, it also means that if you revoke a session, the user can still use it until the access token expires. +Although this makes session verification fast, it also means that if you revoke a session, the user can still use its access token until the token expires. -If you want session verifications to fail immediately after revoking the session, you should force the session to check against the database. -Since you can use this feature on a per API basis, we recommend that you only use it for non-GET APIs since only those are state changing. +If verification must fail immediately after session revocation, force an authoritative database check. +You can enable this per API. Choose endpoints based on the sensitivity of the data or action and your threat model, not +the HTTP method. A confidential `GET` endpoint can require immediate revocation just as much as a state-changing endpoint. ## Before you start -:::caution -For managed service users, please check [the rate limit policy](/docs/deployment/rate-limits) before implementing this feature. If you suspect that you might breach the free limit you can: -- [Email support](mailto:support@supertokens.com) to increase your rate limit. -- Use the `checkDatabase` flag only on certain important APIs. For example, omit using it in any `GET` API as those are not state changing. -- Implement your own method for keeping track of revoked access tokens by using a cache like Redis. +:::warning +Database-backed verification adds a Core request to each protected API call. Managed service users should check the +[current rate limit policy](/deployment/rate-limits) for their plan before enabling it broadly. Apply `checkDatabase` to +every endpoint that requires immediate revocation, including sensitive read endpoints, and capacity-plan for that traffic. ::: --- -## Using `Verify Session` - - - - - - +## Using `Verify Session` + + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -47,17 +48,13 @@ import { SessionRequest } from "supertokens-node/framework/express"; let app = express(); -// highlight-start app.post("/like-comment", verifySession({ checkDatabase: true }), (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - // highlight-end - //.... + let userId = req.session!.getUserId(); + //.... }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -66,26 +63,23 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", - //highlight-start - options: { - pre: [ - { - method: verifySession({ checkDatabase: true }) - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - //highlight-end - //... - } -}) + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession({ checkDatabase: true }), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //... + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -93,35 +87,32 @@ import { SessionRequest } from "supertokens-node/framework/fastify"; let fastify = Fastify(); -//highlight-start -fastify.post("/like-comment", { +fastify.post( + "/like-comment", + { preHandler: verifySession({ checkDatabase: true }), -}, (req: SessionRequest, res) => { + }, + (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - //highlight-end //.... -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEventV2 } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEventV2) { - let userId = awsEvent.session!.getUserId(); - //.... -}; + let userId = awsEvent.session!.getUserId(); + //.... +} -//highlight-next-line exports.handler = verifySession(likeComment, { checkDatabase: true }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -129,17 +120,13 @@ import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); -//highlight-start router.post("/like-comment", verifySession({ checkDatabase: true }), (ctx: SessionContext, next) => { - let userId = ctx.session!.getUserId(); - //highlight-end - //.... + let userId = ctx.session!.getUserId(); + //.... }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -147,105 +134,65 @@ import { verifySession } from "supertokens-node/recipe/session/framework/loopbac import { SessionContext } from "supertokens-node/framework/loopback"; class LikeComment { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @intercept(verifySession({ checkDatabase: true })) - @response(200) - handler() { - let userId = (this.ctx as SessionContext).session!.getUserId(); - //highlight-end - //.... - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession({ checkDatabase: true })) + @response(200) + handler() { + let userId = (this.ctx as SessionContext).session!.getUserId(); + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function likeComment(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ checkDatabase: true })(req, res, next); - }, - req, - res - ) - - let userId = req.session!.getUserId(); - // highlight-end - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + await superTokensNextWrapper( + async (next) => { + await verifySession({ checkDatabase: true })(req, res, next); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let userId = session!.getUserId(); - //.... - return NextResponse.json({}) - }, { checkDatabase: true }); + let userId = req.session!.getUserId(); + //.... } ``` - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new AuthGuard({ checkDatabase: true })) // For more information about this guard please read our NestJS guide. async postExample(@Session() session: SessionContainer): Promise { - //highlight-start let userId = session.getUserId(); - //highlight-end //.... return true; } } ``` - - - - - - - + + + + + + ```go import ( "fmt" @@ -274,10 +221,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -320,10 +265,8 @@ func likeCommentAPI(c *gin.Context) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -353,10 +296,8 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - + + ```go import ( "fmt" @@ -386,57 +327,46 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userID) } ``` - - - - - - - - - -```python + + + + + + +```python check=false reason="Requires surrounding framework application context" from fastapi import Depends from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.fastapi import verify_session -# highlight-start -@app.post('/like_comment') # type: ignore +@app.post('/like_comment') async def like_comment(session: SessionContainer = Depends(verify_session(check_database=True))): user_id = session.get_user_id() - # highlight-end print(user_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import g from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.flask import verify_session -# highlight-start -@app.route('/update-jwt', methods=['POST']) # type: ignore +@app.route('/update-jwt', methods=['POST']) @verify_session(check_database=True) def like_comment(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens user_id = session.get_user_id() - # highlight-end print(user_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding application context" from typing import cast from django.http import HttpRequest @@ -445,32 +375,73 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session -# highlight-start @verify_session(check_database=True) async def like_comment(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() - # highlight-end print(user_id) ``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let userId = session!.getUserId(); + //.... + return NextResponse.json({}); + }, + { checkDatabase: true }, + ); +} +``` - - + + + + + --- ## Using `Get Session` - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; @@ -478,24 +449,22 @@ import Session from "supertokens-node/recipe/session"; let app = express(); app.post("/like-comment", async (req, res, next) => { - try { - let session = await Session.getSession(req, res, { checkDatabase: true }) - - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - //.... - } catch (err) { - next(err); + try { + let session = await Session.getSession(req, res, { checkDatabase: true }); + + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } + //.... + } catch (err) { + next(err); + } }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import Session from "supertokens-node/recipe/session"; @@ -503,25 +472,24 @@ import Session from "supertokens-node/recipe/session"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/like-comment", - method: "post", + path: "/like-comment", + method: "post", - handler: async (req, res) => { - let session = await Session.getSession(req, res, { checkDatabase: true }) + handler: async (req, res) => { + let session = await Session.getSession(req, res, { checkDatabase: true }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - - //... + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } -}) -``` - - + //... + }, +}); +``` + + ```tsx import Fastify from "fastify"; import Session from "supertokens-node/recipe/session"; @@ -529,44 +497,39 @@ import Session from "supertokens-node/recipe/session"; let fastify = Fastify(); fastify.post("/like-comment", async (req, res) => { - let session = await Session.getSession(req, res, { checkDatabase: true }) + let session = await Session.getSession(req, res, { checkDatabase: true }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - //.... + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } + //.... }); ``` - - - - + + ```tsx import Session from "supertokens-node/recipe/session"; import { middleware } from "supertokens-node/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; async function likeComment(awsEvent: SessionEvent) { - let session = await Session.getSession(awsEvent, awsEvent, { checkDatabase: true }) + let session = await Session.getSession(awsEvent, awsEvent, { checkDatabase: true }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } - //.... -}; + //.... +} -//highlight-next-line exports.handler = middleware(likeComment); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import Session from "supertokens-node/recipe/session"; @@ -574,106 +537,71 @@ import Session from "supertokens-node/recipe/session"; let router = new KoaRouter(); router.post("/like-comment", async (ctx, next) => { - let session = await Session.getSession(ctx, ctx, { checkDatabase: true }) + let session = await Session.getSession(ctx, ctx, { checkDatabase: true }); - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } - //.... + //.... }); ``` - - - - + + ```tsx import { inject } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; import Session from "supertokens-node/recipe/session"; class LikeComment { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @response(200) + async handler() { + let session = await Session.getSession(this.ctx, this.ctx, { checkDatabase: true }); - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/like-comment") - @response(200) - async handler() { - let session = await Session.getSession(this.ctx, this.ctx, { checkDatabase: true }) - - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - - //.... + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... } + + //.... + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import Session from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; export default async function likeComment(req: SessionRequest, res: any) { - let session = await superTokensNextWrapper( - async (next) => { - return await Session.getSession(req, res, { checkDatabase: true }); - }, - req, - res - ) - - if (session !== undefined) { - let userId = session.getUserId(); - } else { - // user is not logged in... - } - //.... -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); + let session = await superTokensNextWrapper( + async (next) => { + return await Session.getSession(req, res, { checkDatabase: true }); + }, + req, + res, + ); -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - let userId = session!.getUserId(); - //.... - return NextResponse.json({}) - }, { checkDatabase: true }); + if (session !== undefined) { + let userId = session.getUserId(); + } else { + // user is not logged in... + } + //.... } ``` - - - - + + ```tsx import { Controller, Post, UseGuards, Req, Res } from "@nestjs/common"; import type { Request, Response } from "express"; @@ -681,28 +609,25 @@ import Session from "supertokens-node/recipe/session"; @Controller() export class ExampleController { - @Post('example') + @Post("example") async postExample(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise { - //highlight-start // This should be done inside a parameter decorator, for more information please read our NestJS guide. - const session = await Session.getSession(req, res, { checkDatabase: true }) + const session = await Session.getSession(req, res, { checkDatabase: true }); if (session !== undefined) { const userId = session.getUserId(); } else { // user is not logged in... } - //highlight-end //.... return true; } } ``` - - - - - + + + + ```go import ( "fmt" @@ -735,21 +660,18 @@ func likeCommentAPI(w http.ResponseWriter, r *http.Request) { } } ``` - - - - - - -```python + + + + +```python check=false reason="Requires surrounding framework application context" from fastapi import Request from supertokens_python.recipe.session.asyncio import get_session -@app.post('/like-comment') # type: ignore +@app.post('/like-comment') async def like_comment(request: Request): - # highlight-next-line session = await get_session(request, check_database=True) if session is not None: @@ -758,19 +680,16 @@ async def like_comment(request: Request): else: pass # user is not logged in ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask.wrappers import Request from supertokens_python.recipe.session.syncio import get_session -@app.route('/like-comment', methods=['POST']) # type: ignore +@app.route('/like-comment', methods=['POST']) def like_comment(request: Request): - # highlight-next-line session = get_session(request, check_database=True) if session is not None: @@ -779,10 +698,8 @@ def like_comment(request: Request): else: pass # user is not logged in ``` - - - - + + ```python from django.http import HttpRequest @@ -790,7 +707,6 @@ from supertokens_python.recipe.session.asyncio import get_session async def like_comment(request: HttpRequest): - # highlight-next-line session = await get_session(request, check_database=True) if session is not None: @@ -799,9 +715,43 @@ async def like_comment(request: HttpRequest): else: pass # user is not logged in ``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); - - +export function POST(request: NextRequest) { + return withSession( + request, + async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + let userId = session!.getUserId(); + //.... + return NextResponse.json({}); + }, + { checkDatabase: true }, + ); +} +``` + + + + + + diff --git a/docs/post-authentication/session-management/advanced-workflows/anonymous-session.mdx b/docs/post-authentication/session-management/advanced-workflows/anonymous-session.mdx index a125bd9b32..c1d4b270fe 100644 --- a/docs/post-authentication/session-management/advanced-workflows/anonymous-session.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/anonymous-session.mdx @@ -1,19 +1,11 @@ --- title: Implement anonymous sessions -hide_title: true -sidebar_position: 1 -description: >- - Track user actions with anonymous sessions, create and verify JWTs, and - transfer data post-login. -page_type: guide -recipe: session -category: session-management +description: Track user actions with anonymous sessions, create and verify JWTs, and transfer data post-login. +sidebar: + order: 10 --- - -# Implement anonymous sessions - -## Overview +## Overview With anonymous sessions, you can keep track of user's action / data before they login, and then transfer that data to their post login session. @@ -32,28 +24,28 @@ They can store any information about the user's activity, and they don't occupy Start by creating a JWT like in the next example: - - - + + ```tsx -import Session from "supertokens-node/recipe/session" +import Session from "supertokens-node/recipe/session"; async function createAnonymousJWT(payload: any) { - let jwtResponse = await Session.createJWT({ - key: "value", - // more payload... - }, 315360000); // 10 years lifetime - if (jwtResponse.status === "OK") { - // Send JWT as Authorization header to M2 - return jwtResponse.jwt; - } - throw new Error("Unable to create JWT. Should never come here.") + let jwtResponse = await Session.createJWT( + { + key: "value", + // more payload... + }, + 315360000, + ); // 10 years lifetime + if (jwtResponse.status === "OK") { + // Send JWT as Authorization header to M2 + return jwtResponse.jwt; + } + throw new Error("Unable to create JWT. Should never come here."); } ``` - - - - + + ```go import ( "fmt" @@ -75,13 +67,10 @@ func main() { // Send JWT as Authorization header to M2 } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.session import asyncio from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult @@ -98,10 +87,8 @@ async def create_jwt(): else: raise Exception("Unable to create JWT. Should never come here.") ``` - - - - + + ```python from supertokens_python.recipe.session.syncio import create_jwt from supertokens_python.recipe.jwt.interfaces import CreateJwtOkResult @@ -117,109 +104,102 @@ if isinstance(jwtResponse, CreateJwtOkResult): else: raise Exception("Unable to create JWT. Should never come here.") ``` - - - - - - + + + + - As shown in the code above, you can add any payload you like to the JWT. You can even add a `sub` (`userId`) payload with a random `UUID` if you like, or some user ID with a prefix like `"G-.."` which indicates this is a guest user ID. - You could create your own application middleware which inspects the request and auto adds a JWT to it in the response cookies. This way, whenever a user visits your website and makes an API call, they get a JWT in their cookies, and you can use that JWT to track their activity. -### 2. Send the JWT to the client +### 2. Send the JWT to the client After creating the JWT, you can send it to a user as a cookie. This way you can use it to track the activity during a session. #### Verify the JWT -To check if an anonymous session is valid read through the [manual JWT verification section](/docs/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). +To check if an anonymous session is valid read through the [manual JWT verification section](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). On thing to note here is that in the section about verification using the public key string, you do not need to set `useDynamicAccessTokenSigningKey` to `true`. The token creation process in this scenario uses the static signing key (`kid` starting the `s-..`) by default. -:::caution -You **cannot** use the `verifySession` or `getSession` functions to verify JWTs from anonymous sessions. +:::warning[You **cannot** use the `verifySession` or `getSession` functions to verify JWTs from anonymous sessions.] The `verifySession` and `getSession` check for the presence of certain claims in the JWT (`sessionHandle`, `refreshTokenHash` etc...) that the Session recipe adds for authenticated users. ::: - - ### 3. Transfer the data to a logged in session The idea here is to override the `Session` recipe on the `backend`. Whenever the user logs in or signs up, the data from the anonymous session transfers to the logged-in session. The override attempts to read the request header cookie to get the JWT. It assumes that you have added it to the cookies, verifies it, and then adds the payload to the logged-in session. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // ... - Session.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - createNewSession: async function (input) { - let userId = input.userId; - - const request = SuperTokens.getRequestFromUserContext(input.userContext); - - let jwt: string | undefined - let jwtPayload = {} - - if (request !== undefined) { - jwt = request.getCookieValue("jwt"); - } else { - /** - * This is possible if the function is triggered from the user management dashboard - * - * In this case because we cannot read the JWT, we create a session without the custom - * payload properties - */ - } - - if (jwt !== undefined) { - // verify JWT using a JWT verification library.. - - jwtPayload = { /* ... get from decoded jwt ... */}; - } - - // This goes in the access token, and is available to read on the frontend. - input.accessTokenPayload = { - ...input.accessTokenPayload, - ...jwtPayload - }; - - return originalImplementation.createNewSession(input); - }, - }; - }, + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + createNewSession: async function (input) { + let userId = input.userId; + + const request = SuperTokens.getRequestFromUserContext(input.userContext); + + let jwt: string | undefined; + let jwtPayload = {}; + + if (request !== undefined) { + jwt = request.getCookieValue("jwt"); + } else { + /** + * This is possible if the function is triggered from the user management dashboard + * + * In this case because we cannot read the JWT, we create a session without the custom + * payload properties + */ + } + + if (jwt !== undefined) { + // verify JWT using a JWT verification library.. + + jwtPayload = { + /* ... get from decoded jwt ... */ + }; + } + + // This goes in the access token, and is available to read on the frontend. + input.accessTokenPayload = { + ...input.accessTokenPayload, + ...jwtPayload, + }; + + return originalImplementation.createNewSession(input); }, - // highlight-end - }) - ] + }; + }, + }, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -231,7 +211,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-start Override: &sessmodels.OverrideStruct{ Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { // First we copy the original implementation func @@ -263,7 +242,7 @@ func main() { } else { /** * This is possible if the function is triggered from the user management dashboard - * + * * In this case because we cannot read the JWT, we create a session without the custom * payload properties */ @@ -275,16 +254,14 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo, get_request_from_user_context from supertokens_python.recipe import session from supertokens_python.recipe.session.interfaces import RecipeInterface @@ -348,21 +325,19 @@ def override_functions(original_implementation: RecipeInterface): init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ session.init( - # highlight-start override=session.InputOverrideConfig(functions=override_functions) - # highlight-end ) ], ) ``` - - + + - In the above code snippet, the system reads the JWT from the request header cookie. If it exists, it verifies it and then adds the payload to the logged-in session. - If the JWT doesn't exist, or if the system cannot verify it, it would be safe to ignore it and create the logged-in session anyway. - We assume that the you have saved the JWT in the cookies in with the key of `jwt`. But if not, you can remove the JWT from the request based on how you have saved it. -You can even read the headers from the request. +You can even read the headers from the request. diff --git a/docs/post-authentication/session-management/advanced-workflows/customize-error-handling.mdx b/docs/post-authentication/session-management/advanced-workflows/customize-error-handling.mdx index 8fe51954df..bf884448df 100644 --- a/docs/post-authentication/session-management/advanced-workflows/customize-error-handling.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/customize-error-handling.mdx @@ -1,16 +1,10 @@ --- title: Customize error handling -hide_title: true -sidebar_position: 3 description: Customize errors thrown by the Session recipe in your SuperTokens integration. -page_type: guide -recipe: session -category: session-management +sidebar: + order: 30 --- - -# Customize error handling - ## Overview The following page shows the errors that the SuperTokens Session recipe throws and how you can customize them. @@ -22,38 +16,34 @@ The following page shows the errors that the SuperTokens Session recipe throws a The system generates the error when someone accesses a protected backend API without a session. The default behavior is to clear session tokens (if any) and send a 401 to the frontend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - errorHandlers: { - onUnauthorised: async (message, request, response, userContext) => { - // TODO: Write your own logic and then send a 401 response to the frontend - }, - } - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + errorHandlers: { + onUnauthorised: async (message, request, response, userContext) => { + // TODO: Write your own logic and then send a 401 response to the frontend + }, + }, + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -67,23 +57,20 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ErrorHandlers: &sessmodels.ErrorHandlers{ OnUnauthorised: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 401 response to the frontend return nil }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse @@ -94,21 +81,18 @@ async def unauthorised_callback(req: BaseRequest, err: str, response: BaseRespon init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start error_handlers=session.InputErrorHandlers( on_unauthorised=unauthorised_callback ) - # highlight-end ) ] ) ``` - - - + + --- @@ -117,38 +101,34 @@ init( The system generates the error when someone accesses a protected backend API with a session that doesn't pass the claim validators. The default behavior is to send a 403 to the frontend with the errors included in the body. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - errorHandlers: { - onInvalidClaim: async (validatorErrors, request, response, userContext) => { - // TODO: Write your own logic and then send a 403 response to the frontend - }, - } - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + errorHandlers: { + onInvalidClaim: async (validatorErrors, request, response, userContext) => { + // TODO: Write your own logic and then send a 403 response to the frontend + }, + }, + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -163,23 +143,20 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ErrorHandlers: &sessmodels.ErrorHandlers{ OnInvalidClaim: func(validationErrors []claims.ClaimValidationError, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 403 response to the frontend return nil }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.recipe.session.exceptions import ClaimValidationError @@ -193,21 +170,18 @@ async def invalid_claim_callback(req: BaseRequest, invalid_claims: List[ClaimVal init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start error_handlers=session.InputErrorHandlers( on_invalid_claim=invalid_claim_callback ) - # highlight-end ) ] ) ``` - - - + + --- @@ -217,38 +191,34 @@ The system generates the error when the system detects a [session hijacking](htt This happens through the use of [rotating refresh tokens](https://supertokens.com/blog/the-best-way-to-securely-manage-user-sessions). The default behavior is to revoke the session and send a `401` to the frontend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - errorHandlers: { - onTokenTheftDetected: async (sessionHandle, userId, req, res, userContext) => { - // TODO: Write your own logic and then send a 401 response to the frontend - }, - } - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + errorHandlers: { + onTokenTheftDetected: async (sessionHandle, userId, req, res, userContext) => { + // TODO: Write your own logic and then send a 401 response to the frontend + }, + }, + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -262,7 +232,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ErrorHandlers: &sessmodels.ErrorHandlers{ OnTokenTheftDetected: func(sessionHandle, userID string, req *http.Request, res http.ResponseWriter) error { @@ -270,16 +239,14 @@ func main() { return nil }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse @@ -291,22 +258,19 @@ async def token_theft_detected_callback(req: BaseRequest, session_handle: str, u init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - - framework='...', # type: ignore + + framework='...', recipe_list=[ session.init( - # highlight-start error_handlers=session.InputErrorHandlers( on_token_theft_detected=token_theft_detected_callback ) - # highlight-end ) ] ) ``` - - - + + --- @@ -316,38 +280,34 @@ Thrown when the access token expires or is invalid. The session refresh endpoint can also throw this if multiple access tokens are present in the request cookies. The default behavior is to send a `401` to the frontend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - errorHandlers: { - onTryRefreshToken: async (message, request, response, userContext) => { - // TODO: Write your own logic and then send a 401 response to the frontend - }, - } - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + errorHandlers: { + onTryRefreshToken: async (message, request, response, userContext) => { + // TODO: Write your own logic and then send a 401 response to the frontend + }, + }, + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -361,23 +321,20 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ErrorHandlers: &sessmodels.ErrorHandlers{ OnTryRefreshToken: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 401 response to the frontend return nil }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse @@ -388,21 +345,18 @@ async def try_refresh_callback(req: BaseRequest, err: str, response: BaseRespons init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start error_handlers=session.InputErrorHandlers( on_try_refresh_token=try_refresh_callback ) - # highlight-end ) ] ) ``` - - - + + --- @@ -411,38 +365,34 @@ init( Thrown when the refresh session API clears session cookies from the `olderCookieDomain` because it found multiple access tokens in the request cookies. See [this issue](https://github.com/supertokens/supertokens-node/issues/826) for more information. The default behavior is to send a 200 to the frontend. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - errorHandlers: { - onClearDuplicateSessionCookies: async (message, request, response, userContext) => { - // TODO: Write your own logic and then send a 200 response to the frontend - }, - } - //highlight-end - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + errorHandlers: { + onClearDuplicateSessionCookies: async (message, request, response, userContext) => { + // TODO: Write your own logic and then send a 200 response to the frontend + }, + }, + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -456,23 +406,20 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start ErrorHandlers: &sessmodels.ErrorHandlers{ OnClearDuplicateSessionCookies: func(message string, req *http.Request, res http.ResponseWriter) error { // TODO: Write your own logic and then send a 200 response to the frontend return nil }, }, - //highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest, BaseResponse @@ -483,18 +430,15 @@ async def on_clear_duplication_session_cookies_callback(req: BaseRequest, err: s init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start error_handlers=session.InputErrorHandlers( on_clear_duplicate_session_cookies=on_clear_duplication_session_cookies_callback ) - # highlight-end ) ] ) ``` - - - + + diff --git a/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors.mdx b/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors.mdx index d555d92d54..b855d7c78f 100644 --- a/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/disable-frontend-interceptors.mdx @@ -1,18 +1,10 @@ --- title: Disable frontend network interceptors -hide_title: true -sidebar_position: 6 -description: >- - Disable frontend network interceptors to manage session tokens and request - headers manually in projects. -page_type: guide -recipe: session -category: session-management +description: Disable frontend network interceptors to manage session tokens and request headers manually in projects. +sidebar: + order: 50 --- - -# Disable frontend network interceptors - ## Overview SuperTokens frontend SDKs add interceptors to networking libraries to: @@ -26,226 +18,224 @@ Whilst this helps for greenfield projects, for existing projects, you may want t ### 1. Update the frontend configuration - + - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import Session from "supertokens-auth-react/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUISession.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + - - - + + - - - - - - - + + + + +You can use the `doesSessionExist` function to check if a session exists. + + +:::note[At the moment this feature is not supported through the Android SDK.] +::: + + +:::note[At the moment this feature is not supported through the iOS SDK.] +::: + + +:::note[At the moment this feature is not supported through the Flutter SDK.] +::: + + + + + + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokensSession.init({ - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` - - - - - - - - - - - - -You can use the `doesSessionExist` function to check if a session exists. - + + + + + + ```tsx import SuperTokens from "supertokens-react-native"; SuperTokens.init({ - apiDomain: "...", - override: { - functions: (oI) => { - return { - ...oI, - shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { - try { - let urlObj = new URL(url); - if (!urlObj.pathname.startsWith("/auth")) { - return false; - } - } catch (ignored) { } - return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); - } + apiDomain: "...", + override: { + functions: (oI) => { + return { + ...oI, + shouldDoInterceptionBasedOnUrl: (url, apiDomain, sessionTokenBackendDomain) => { + try { + let urlObj = new URL(url); + if (!urlObj.pathname.startsWith("/auth")) { + return false; } - } - } -}) + } catch (ignored) {} + return oI.shouldDoInterceptionBasedOnUrl(url, apiDomain, sessionTokenBackendDomain); + }, + }; + }, + }, +}); ``` + + - - - - -:::note -At the moment this feature is not supported through the Android SDK. -::: - - - - - -:::note -At the moment this feature is not supported through the iOS SDK. -::: - - - - - -:::note -At the moment this feature is not supported through the Flutter SDK. -::: + + - + + - + + + + - - - + In the code above, the `shouldDoInterceptionBasedOnUrl` function is overridden to only allow interception for all API calls that start with `/auth` in their path. This ensures that API calls made from frontend SDKs (like sign out) continue to use the session tokens as expected by backend APIs. It also allows you to take control of how you want to attach session tokens to your own API calls (ones that have a path that don't start with `/auth`). -If you want to also change how session tokens attach to API calls (like sign out), you can return `false` from the function override. Then, attach custom session headers using the [pre-API hook function](/docs/references/frontend-sdks/hooks#pre-api-hook) on the frontend. +If you want to also change how session tokens attach to API calls (like sign out), you can return `false` from the function override. Then, attach custom session headers using the [pre-API hook function](/references/frontend-sdks/hooks#pre-api-hook) on the frontend. diff --git a/docs/post-authentication/session-management/advanced-workflows/in-iframe.mdx b/docs/post-authentication/session-management/advanced-workflows/in-iframe.mdx index 67f4381149..67fed8ceca 100644 --- a/docs/post-authentication/session-management/advanced-workflows/in-iframe.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/in-iframe.mdx @@ -1,18 +1,10 @@ --- title: Usage inside an iframe -hide_title: true -sidebar_position: 6 -description: >- - Embed your website in an iframe with secure session management and custom - storage handlers. -page_type: guide -recipe: session -category: session-management +description: Embed your website in an iframe with secure session management and custom storage handlers. +sidebar: + order: 60 --- - -# Usage inside an iframe - ## Overview If your website can embed in an iframe that other websites consume, update your configuration based on this guide. @@ -32,179 +24,157 @@ If the sites where your iframe can embed share the same top-level domain as the - Switch to using header based auth - Provide a custom `windowHandler` and a custom `cookieHandler` to ensure that the app works on safari and chrome incognito. These handlers switch from using `document.cookies` to `localstorage` to store tokens on the frontend (since safari doesn't allow access to `document.cookies` in iframes), and use in-memory storage for chrome incognito (since chrome incognito doesn't even allow access to `localstorage`). You can find implementations of these handlers [here (`windowHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/windowHandler.js) and [here (`cookieHandler`)](https://github.com/SuperTokens/supertokens-auth-react/blob/master/examples/with-next-iframe/config/cookieHandler.js). - + - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; -declare let cookieHandler: any // REMOVE_FROM_OUTPUT -declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ - cookieHandler, - windowHandler, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-start - tokenTransferMethod: "header", - isInIframe: true - // highlight-end - }) - ] + cookieHandler, + windowHandler, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", + isInIframe: true, + }), + ], }); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -declare let cookieHandler: any // REMOVE_FROM_OUTPUT -declare let windowHandler: any // REMOVE_FROM_OUTPUT - + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokensUIInit({ - cookieHandler, - windowHandler, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUISession.init({ - // highlight-start - tokenTransferMethod: "header", - isInIframe: true - // highlight-end - }) - ] + cookieHandler, + windowHandler, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUISession.init({ + tokenTransferMethod: "header", + isInIframe: true, + }), + ], }); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; -declare let cookieHandler: any // REMOVE_FROM_OUTPUT -declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ - cookieHandler, - windowHandler, - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - tokenTransferMethod: "header", - isInIframe: true - // highlight-end - }) - ], -}) + cookieHandler, + windowHandler, + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", + isInIframe: true, + }), + ], +}); ``` + + - - - - - + - + - - - - -```tsx + + +:::warning[Not applicable to mobile apps] +::: + + + + + + + +```tsx check=false reason="Requires SDK globals from surrounding application" import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; -declare let cookieHandler: any // REMOVE_FROM_OUTPUT -declare let windowHandler: any // REMOVE_FROM_OUTPUT SuperTokens.init({ - cookieHandler, - windowHandler, - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - tokenTransferMethod: "header", - isInIframe: true - // highlight-end - }) - ], -}) + cookieHandler, + windowHandler, + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", + isInIframe: true, + }), + ], +}); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -declare let cookieHandler: any // REMOVE_FROM_OUTPUT -declare let windowHandler: any // REMOVE_FROM_OUTPUT + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ - cookieHandler, - windowHandler, - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - supertokensSession.init({ - // highlight-start - tokenTransferMethod: "header", - isInIframe: true - // highlight-end - }) - ], -}) + cookieHandler, + windowHandler, + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + supertokensSession.init({ + tokenTransferMethod: "header", + isInIframe: true, + }), + ], +}); ``` + + + + - - - - - - - -:::caution -Not applicable to mobile apps -::: + + - - - + -:::caution -Because of the restrictions on access to storage on Chrome incognito, you must use in-memory storage to store the tokens on the frontend. This in turn implies that if the user refreshes the page, or if your app does a full page navigation, the user logs out. +:::warning[Because of the restrictions on access to storage on Chrome incognito, you must use in-memory storage to store the tokens on the frontend. This in turn implies that if the user refreshes the page, or if your app does a full page navigation, the user logs out.] ::: diff --git a/docs/post-authentication/session-management/advanced-workflows/meta.ts b/docs/post-authentication/session-management/advanced-workflows/meta.ts new file mode 100644 index 0000000000..cdd4f45f34 --- /dev/null +++ b/docs/post-authentication/session-management/advanced-workflows/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Advanced Workflows", + icon: "workflow", + order: 60, +}); diff --git a/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints.mdx b/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints.mdx index 2f8381a683..0518b21eef 100644 --- a/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints.mdx @@ -1,19 +1,10 @@ --- title: Work with multiple API endpoints -hide_title: true -sidebar_position: 5 -description: >- - Configure sessions for multiple API endpoints using cookie and header-based - authentication. -page_type: guide -recipe: session -category: session-management +description: Configure sessions for multiple API endpoints using cookie and header-based authentication. +sidebar: + order: 40 --- - -# Work with multiple API endpoints - - ## Overview To enable use of sessions for multiple API endpoints, you need to update the configuration on both the frontend and backend. @@ -29,40 +20,37 @@ If using header based auth, please skip to step 3. ### 1. Set the cookie domain in the backend configuration -:::caution no-title +:::warning This step is only applicable for cookie based authentication. ::: Set the `cookieDomain` value to be the common top level domain. For example, if your API endpoints are `{"api.example.com", "api2.example.com", "api3.example.com"}`, the common portion of these endpoints is `".example.com"` (The dot is important). You would need to set the following: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - cookieDomain: ".example.com", - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + cookieDomain: ".example.com", + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -71,89 +59,78 @@ import ( ) func main() { - // highlight-next-line cookieDomain := ".example.com" supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line CookieDomain: &cookieDomain, }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start cookie_domain='.example.com' - # highlight-end ) ] ) ``` - - - + + The above sets the session cookies' domain to `example.com`, allowing them to send to `*.example.com`. -:::note -Whilst the `cookieDomain` can start with a leading `.`, the value of the `apiDomain` in `appInfo` must point to an exact API domain only. This should be the API in which you want to expose all the auth related endpoints (for example `/auth/signin`). +:::note[Whilst the `cookieDomain` can start with a leading `.`, the value of the `apiDomain` in `appInfo` must point to an exact API domain only. This should be the API in which you want to expose all the auth related endpoints (for example `/auth/signin`).] -For local development, you should not set the `cookieDomain` to an IP address-based domain, or `.localhost` - browsers reject these cookies. Instead, you should [alias `localhost` to a named domain and use that](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). +For local development, you should not set the `cookieDomain` to an IP address-based domain, or `.localhost` - browsers reject these cookies. Instead, you should [alias `localhost` to a named domain and use that](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: -### 2. Set the older cookie domain in the backend configuration +### 2. Set the older cookie domain in the backend configuration -:::caution no-title +:::warning This step is only applicable for cookie based authentication. ::: To avoid locking out users with existing sessions (they get a 500 error when trying to refresh their session), set `olderCookieDomain` to match your previous `cookieDomain`. If your `cookieDomain` was not set, you can use an empty string. However, if you don't have any existing sessions, you can skip this step entirely. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - cookieDomain: ".example.com", - // highlight-next-line - olderCookieDomain: "" // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + cookieDomain: ".example.com", + olderCookieDomain: "", // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -162,212 +139,191 @@ import ( ) func main() { - // highlight-next-line cookieDomain := ".example.com" olderCookieDomain := "" // Set to an empty string if your previous cookieDomain was unset. Otherwise, use your old cookieDomain value. supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line CookieDomain: &cookieDomain, - // highlight-next-line OlderCookieDomain: &olderCookieDomain, }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start cookie_domain='.example.com', older_cookie_domain='' # Set to an empty string if your previous cookie_domain was unset. Otherwise, use your old cookie_domain value. - # highlight-end ) ] ) ``` + + - - - -:::caution -- If `olderCookieDomain` isn't set, users with older sessions get a 500 error from the session refresh endpoint, locking them out. This continues until you set `olderCookieDomain` correctly or they clear their cookies. +:::warning[- If `olderCookieDomain` isn't set, users with older sessions get a 500 error from the session refresh endpoint, locking them out. This continues until you set `olderCookieDomain` correctly or they clear their cookies.] - Keep the value set for `olderCookieDomain` for 1 year because the cookie lifetime of the access token on the frontend is 1 year (even though the JWT expiry is a few hours). -- If you have changed the `cookieDomain` more than once within one year, to prevent a stuck state, switch to [header-based auth](https://supertokens.com/docs/session/common-customizations/sessions/token-transfer-method#backend-configuration-optional) for all your clients. The important thing here is that you have to set the backend configuration to header even though that doc says it's optional. This ensures that all clients use header-based auth. +- If you have changed the `cookieDomain` more than once within one year, to prevent a stuck state, switch to [header-based auth](https://supertokens.com/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication) for all your clients. The important thing here is that you have to set the backend configuration to header even though that doc says it's optional. This ensures that all clients use header-based auth. - Changing the `cookieDomain` can cause a temporary spike in requests, even if you set the `olderCookieDomain` correctly. This happens because older sessions, with older cookie domain, require additional refresh calls to clear their old cookies and set new ones. This spike is a one-time event and should not recur after the update. ::: -:::info -Set the `olderCookieDomain` value to prevent clients from having multiple session cookies from different domains. This can happen when cookies from a previous domain are still valid and sent with requests. For instance, if your previous `cookieDomain` was `api.example.com` and the new one is `.example.com`, both sets of cookies would send to the `apiDomain` `api.example.com`, leading to an inconsistent state. This can cause issues until you clear the older cookies. Setting `olderCookieDomain` in the configuration ensures that the SuperTokens SDK can automatically remove these older cookies. +:::info[Set the `olderCookieDomain` value to prevent clients from having multiple session cookies from different domains. This can happen when cookies from a previous domain are still valid and sent with requests. For instance, if your previous `cookieDomain` was `api.example.com` and the new one is `.example.com`, both sets of cookies would send to the `apiDomain` `api.example.com`, leading to an inconsistent state. This can cause issues until you clear the older cookies. Setting `olderCookieDomain` in the configuration ensures that the SuperTokens SDK can automatically remove these older cookies.] ::: -### 3. Update the frontend configuration +### 3. Update the frontend configuration Set the same value for `sessionTokenBackendDomain` on the frontend. This allows the frontend SDK to apply interception and automatic refreshing across all your API calls: - + - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - sessionTokenBackendDomain: ".example.com" - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + sessionTokenBackendDomain: ".example.com", + }), + ], }); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. -```tsx -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUISession.init({ - // highlight-next-line - sessionTokenBackendDomain: ".example.com" - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUISession.init({ + sessionTokenBackendDomain: ".example.com", + }), + ], }); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - sessionTokenBackendDomain: ".example.com" - }), - ], + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + sessionTokenBackendDomain: ".example.com", + }), + ], }); ``` + + - - - - - + - + - - - - + + + + ```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - sessionTokenBackendDomain: ".example.com" - }), - ], + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + sessionTokenBackendDomain: ".example.com", + }), + ], }); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - supertokensSession.init({ - sessionTokenBackendDomain: ".example.com", - }) - ], + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + supertokensSession.init({ + sessionTokenBackendDomain: ".example.com", + }), + ], }); ``` - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; SuperTokens.init({ - apiDomain: "...", - sessionTokenBackendDomain: ".example.com" + apiDomain: "...", + sessionTokenBackendDomain: ".example.com", }); ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -375,30 +331,25 @@ import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() - + SuperTokens.Builder(this, "...") - // highlight-next-line .sessionTokenBackendDomain(".example.com") .build() } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { do { try SuperTokens.initialize( apiDomain: "...", - // highlight-next-line sessionTokenBackendDomain: ".example.com" ) } catch SuperTokensError.initError(let message) { @@ -409,14 +360,11 @@ fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { return true } - + } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -427,13 +375,11 @@ void initialiseSuperTokens() { ); } ``` + + + + - - - - - - - + diff --git a/docs/post-authentication/session-management/advanced-workflows/user-impersonation.mdx b/docs/post-authentication/session-management/advanced-workflows/user-impersonation.mdx index e2510465c6..00b26930b2 100644 --- a/docs/post-authentication/session-management/advanced-workflows/user-impersonation.mdx +++ b/docs/post-authentication/session-management/advanced-workflows/user-impersonation.mdx @@ -1,19 +1,10 @@ --- title: Implement user impersonation -hide_title: true -sidebar_position: 2 -description: >- - Enable user impersonation for testing and support by creating an - admin-protected API endpoint. -page_type: guide -recipe: session -category: session-management +description: Enable user impersonation for testing and support by creating an admin-protected API endpoint. +sidebar: + order: 20 --- - - -# Implement user impersonation - ## Overview Impersonating a user allows you to login as them without using their credentials. @@ -23,25 +14,40 @@ This guide shows you how to achieve this by only allowing a certain type of user ## Before you start -:::caution -Since this feature allows you to login as any user in your application, only admins or custom support staff should use it. -You can use any method to protect this API. (API Key, specific user roles, IP address validation, etc.) +:::danger[Impersonation is a privileged operation] +Require a valid staff session and enforce an explicit backend role or permission check before reading the target identifier, looking up the target, or creating a session. Hiding the UI, checking a role in the browser, restricting access to approved IP addresses, or knowing the endpoint URL is not authorization. ::: +For production use, apply these additional controls: + +- Require a recent step-up authentication before each impersonation starts. For example, require the staff member to complete MFA again. +- Require a reason and write append-only audit events for every attempt and outcome. Include the actor's user ID, target user ID, reason, timestamp, outcome, and impersonation session handle. Do not rely only on a custom access token claim as the audit record. Fail closed if the start event cannot be recorded. +- Set a short maximum duration. Enforce the deadline on the backend, revoke the impersonation session when it expires, and provide an explicit way to terminate it early. Monitor and alert on unusual impersonation activity. +- Decide which targets and actions staff may access while impersonating. For example, prevent impersonation of other administrators and block credential, MFA, payment, and destructive account changes unless your policy explicitly allows them. + ## Steps ### 1. Create the impersonation endpoint -Create a new API endpoint that accepts some form of identifier (email, phone number, user ID, etc.) and creates a new impersonation session for that user. +Create a new API endpoint that accepts a stable user ID and creates a new impersonation session for that user. If you instead use an email address, phone number, or other account information, require the lookup to return exactly one user; never select the first of multiple matches. -In order for this to work, admins need to first log in to the application as themselves. Once they create their session (like any regular user's session), they can call the API via a frontend UI that's only shown to them. You can detect the admin role on the frontend by seeing [this guide](/docs/additional-verification/user-roles/protecting-routes#protecting-frontend-routes). +In order for this to work, admins need to first log in to the application as themselves. Once they create their session (like any regular user's session), they can call the API via a frontend UI that's only shown to them. You can detect the admin role on the frontend by seeing [this guide](/additional-verification/user-roles/protecting-routes#protect-frontend-routes). - - - - + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -52,36 +58,34 @@ import UserRoles from "supertokens-node/recipe/userroles"; let app = express(); app.post( - "/impersonate", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ], - }), - async (req, res) => { - + "/impersonate", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { - email + email, }); - if (user.length === 0) { - throw new Error("User does not exist"); + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, + isImpersonation: true, }); res.json({ message: "Impersonation successful!" }); -}) + }, +); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -92,43 +96,41 @@ import UserRoles from "supertokens-node/recipe/userroles"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/impersonate", - method: "post", - options: { - pre: [ - { - method: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ], - }), - }, - ], - }, - handler: async (req, res) => { - - let email = "..."; // read from request body + path: "/impersonate", + method: "post", + options: { + pre: [ + { + method: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + }, + ], + }, + handler: async (req, res) => { + let email = "..."; // read from request body - let user = await supertokens.listUsersByAccountInfo("public", { - email - }); + let user = await supertokens.listUsersByAccountInfo("public", { + email, + }); - if (user.length === 0) { - throw new Error("User does not exist"); - } + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); + } - await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, - }); + await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { + isImpersonation: true, + }); - return res.response({ message: "Impersonation successful!" }).code(200); - }, + return res.response({ message: "Impersonation successful!" }).code(200); + }, }); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -139,78 +141,74 @@ import UserRoles from "supertokens-node/recipe/userroles"; let fastify = Fastify(); fastify.post( - "/impersonate", - { - preHandler: verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => [ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ], - }), - }, - async (req, res) => { - + "/impersonate", + { + preHandler: verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + }, + async (req, res) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { - email + email, }); - if (user.length === 0) { - throw new Error("User does not exist"); + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, + isImpersonation: true, }); res.send({ message: "Impersonation successful!" }); -}) + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; -import { middleware } from "supertokens-node/framework/awsLambda" +import { middleware } from "supertokens-node/framework/awsLambda"; import Session from "supertokens-node/recipe/session"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; async function impersonate(awsEvent: SessionEvent) { - let email = "..."; // read from request body + let email = "..."; // read from request body - let user = await supertokens.listUsersByAccountInfo("public", { - email - }); + let user = await supertokens.listUsersByAccountInfo("public", { + email, + }); - if (user.length === 0) { - throw new Error("User does not exist"); - } + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); + } - await Session.createNewSession(awsEvent, awsEvent, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, - }); + await Session.createNewSession(awsEvent, awsEvent, "public", user[0].loginMethods[0].recipeUserId, { + isImpersonation: true, + }); - return { - body: JSON.stringify({ message: "Impersonation successful!" }), - statusCode: 200, - }; + return { + body: JSON.stringify({ message: "Impersonation successful!" }), + statusCode: 200, + }; } exports.handler = verifySession(impersonate, { - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ]) + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], }); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -221,35 +219,34 @@ import UserRoles from "supertokens-node/recipe/userroles"; let router = new KoaRouter(); router.post( - "/impersonate", - verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ]) - }), - async (ctx, next) => { + "/impersonate", + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + async (ctx, next) => { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { - email + email, }); - if (user.length === 0) { - throw new Error("User does not exist"); + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); } await Session.createNewSession(ctx, ctx, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, + isImpersonation: true, }); ctx.body = { message: "Impersonation successful!" }; -}) + }, +); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; @@ -259,45 +256,41 @@ import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; class Login { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) { } - @post("/impersonate") - @intercept(verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ]) - })) - @response(200) - async handler() { - let email = "..."; // read from request body - - let user = await supertokens.listUsersByAccountInfo("public", { - email - }); + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/impersonate") + @intercept( + verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + ) + @response(200) + async handler() { + let email = "..."; // read from request body - if (user.length === 0) { - throw new Error("User does not exist"); - } + let user = await supertokens.listUsersByAccountInfo("public", { + email, + }); - await Session.createNewSession(this.ctx, this.ctx, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, - }); - return { message: "Impersonation successful!" }; + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); } + + await Session.createNewSession(this.ctx, this.ctx, "public", user[0].loginMethods[0].recipeUserId, { + isImpersonation: true, + }); + return { message: "Impersonation successful!" }; + } } ``` - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { createNewSession } from "supertokens-node/recipe/session"; import { SessionRequest } from "supertokens-node/framework/express"; @@ -305,89 +298,51 @@ import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; export default async function impersonate(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ]) - })(req, res, next); - }, - req, - res - ) - - let email = "..."; // read from request body - - let user = await supertokens.listUsersByAccountInfo("public", { - email - }); - - if (user.length === 0) { - throw new Error("User does not exist"); - } - - await superTokensNextWrapper( - async (next) => { - await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, - }); - }, - req, - res - ); - res.json({ - message: "Impersonation successful!" - }); -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; -import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; -import { createNewSession } from "supertokens-node/recipe/session"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(req: NextRequest) { - return withPreParsedRequestResponse(req, async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { - let email = "..."; // read from request body - - let users = await SuperTokens.listUsersByAccountInfo("public", { email }); + await superTokensNextWrapper( + async (next) => { + await verifySession({ + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + })(req, res, next); + }, + req, + res, + ); - if (users.length === 0) { - throw new Error("User does not exist"); - } + let email = "..."; // read from request body - const session = await createNewSession(baseRequest, baseResponse, "public", users[0].loginMethods[0].recipeUserId, { - isImpersonation: true, - }); + let user = await supertokens.listUsersByAccountInfo("public", { + email, + }); - return NextResponse.json({ message: "Impersonation successful" }); - }); + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); + } - + await superTokensNextWrapper( + async (next) => { + await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { + isImpersonation: true, + }); + }, + req, + res, + ); + res.json({ + message: "Impersonation successful!", + }); } ``` - - - - -```ts + + +```ts check=false reason="Requires surrounding framework application context" import { Controller, Post, Res, Req, UseGuards } from "@nestjs/common"; import type { Response, Request } from "express"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; import { createNewSession, SessionContainer, SessionClaimValidator } from "supertokens-node/recipe/session"; import supertokens from "supertokens-node"; import UserRoles from "supertokens-node/recipe/userroles"; @@ -396,37 +351,37 @@ import UserRoles from "supertokens-node/recipe/userroles"; export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. @Post("impersonate") - @UseGuards(new AuthGuard({ - overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => ([ - ...globalValidators, - UserRoles.UserRoleClaim.validators.includes("admin") - ]) - })) + @UseGuards( + new AuthGuard({ + overrideGlobalClaimValidators: async (globalValidators: SessionClaimValidator[]) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }), + ) async postLogin(@Req() req: Request, @Res() res: Response): Promise<{ message: string }> { let email = "..."; // read from request body let user = await supertokens.listUsersByAccountInfo("public", { - email + email, }); - if (user.length === 0) { - throw new Error("User does not exist"); + if (user.length !== 1) { + throw new Error("Identifier does not uniquely identify a user"); } await createNewSession(req, res, "public", user[0].loginMethods[0].recipeUserId, { - isImpersonation: true, + isImpersonation: true, }); return { message: "Impersonation successful!" }; } } ``` - - - - - - + + + + ```go import ( "net/http" @@ -480,14 +435,11 @@ func impersonate(w http.ResponseWriter, r *http.Request) { // Send 200 success to client } ``` - - - - - - - -```python + + + + +```python check=false reason="Requires surrounding framework application context" from fastapi import Depends, Request from fastapi.responses import JSONResponse @@ -499,7 +451,7 @@ from supertokens_python.recipe.userroles import UserRoleClaim from supertokens_python.types.base import AccountInfoInput -@app.post("/impersonate") # type: ignore +@app.post("/impersonate") async def impersonate( request: Request, session: SessionContainer = Depends( @@ -517,8 +469,8 @@ async def impersonate( # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) - if len(user) == 0: - # return a 400 error to the client + if len(user) != 1: + # return a 400 error because the identifier is missing or ambiguous return await create_new_session( @@ -531,11 +483,9 @@ async def impersonate( return JSONResponse({"message": "Impersonation complete!"}) ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import jsonify from flask.wrappers import Request @@ -546,7 +496,7 @@ from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput -@app.route("/impersonate", methods=["POST"]) # type: ignore +@app.route("/impersonate", methods=["POST"]) @verify_session( # We add the UserRoleClaim's includes validator override_global_claim_validators=lambda global_validators, @@ -559,8 +509,8 @@ def login(request: Request): # we use the email password recipe here, but you can use the recipe you use user = list_users_by_account_info("public", AccountInfoInput(email=email)) - if len(user) == 0: - # return a 400 error to the client + if len(user) != 1: + # return a 400 error because the identifier is missing or ambiguous return create_new_session( @@ -573,10 +523,8 @@ def login(request: Request): return jsonify({"message": "Impersonation complete!"}) ``` - - - - + + ```python from django.http import HttpRequest, JsonResponse @@ -599,8 +547,8 @@ async def impersonate(request: HttpRequest): # we use the email password recipe here, but you can use the recipe you use user = await list_users_by_account_info("public", AccountInfoInput(email=email)) - if len(user) == 0: - # return a 400 error to the client + if len(user) != 1: + # return a 400 error because the identifier is missing or ambiguous return await create_new_session( @@ -613,21 +561,244 @@ async def impersonate(request: HttpRequest): return JsonResponse({"message": "User logged in!"}) ``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context and application audit logging" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withPreParsedRequestResponse } from "supertokens-node/nextjs"; +import { CollectingResponse, PreParsedRequest } from "supertokens-node/framework/custom"; +import Session, { createNewSession } from "supertokens-node/recipe/session"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import { backendConfig } from "@/app/config/backend"; +import { + assertRecentImpersonationStepUp, + createImpersonationAttemptId, + getAuthorizedImpersonationTenant, + getImpersonationExpiry, + recordImpersonationAuditEvent, + registerImpersonationExpiry, +} from "@/app/auth/impersonation-security"; + +SuperTokens.init(backendConfig()); + +function copyCollectedCredentials(source: CollectingResponse, destination: CollectingResponse) { + source.headers.forEach((value, key) => destination.setHeader(key, value, false)); + for (const cookie of source.cookies) { + destination.setCookie( + cookie.key, + cookie.value, + cookie.domain, + cookie.secure, + cookie.httpOnly, + cookie.expires, + cookie.path, + cookie.sameSite, + ); + } +} - - - - +export function POST(request: NextRequest) { + return withPreParsedRequestResponse( + request, + async (baseRequest: PreParsedRequest, baseResponse: CollectingResponse) => { + const actorSession = await Session.getSession(baseRequest, baseResponse, { + sessionRequired: true, + overrideGlobalClaimValidators: async (globalValidators) => [ + ...globalValidators, + UserRoles.UserRoleClaim.validators.includes("admin"), + ], + }); + + const actorUserId = actorSession.getUserId(); + const attemptId = createImpersonationAttemptId(); + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + outcome: "ATTEMPT_STARTED", + }); + + try { + await assertRecentImpersonationStepUp(actorSession); + } catch { + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + outcome: "STEP_UP_FAILED", + }); + return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 }); + } + + // This application-owned helper derives allowed tenants from trusted + // server-side staff entitlements. It must not trust a request-body tenant ID. + let tenantId: string; + try { + tenantId = await getAuthorizedImpersonationTenant({ + actorUserId, + actorSessionTenantId: actorSession.getTenantId(), + }); + } catch { + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + outcome: "TENANT_AUTHORIZATION_FAILED", + }); + return NextResponse.json({ message: "Impersonation request denied" }, { status: 403 }); + } + + // Authorization and step-up have succeeded. Only now read the target and reason. + let targetRecipeUserId = "..."; // validate and read a stable recipe user ID from the request body + let reason = "..."; // require a non-empty support or incident reason + let targetUser: Awaited>; + try { + targetUser = await SuperTokens.getUser(targetRecipeUserId); + } catch { + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + tenantId, + reason, + outcome: "TARGET_LOOKUP_FAILED", + }); + return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 }); + } + const targetLoginMethod = targetUser?.loginMethods.find( + (loginMethod) => loginMethod.recipeUserId.getAsString() === targetRecipeUserId, + ); + + if (!targetUser || !targetLoginMethod?.tenantIds.includes(tenantId)) { + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + targetRecipeUserId, + tenantId, + reason, + outcome: "TARGET_DENIED", + }); + return NextResponse.json({ message: "Impersonation request denied" }, { status: 400 }); + } + + let expiresAt: number; + try { + expiresAt = await getImpersonationExpiry({ actorUserId, tenantId }); + } catch { + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + targetUserId: targetUser.id, + targetRecipeUserId, + tenantId, + reason, + outcome: "EXPIRY_DERIVATION_FAILED", + }); + return NextResponse.json({ message: "Impersonation request denied" }, { status: 500 }); + } + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + targetUserId: targetUser.id, + targetRecipeUserId, + tenantId, + reason, + outcome: "ATTEMPT_APPROVED", + expiresAt, + }); + + const stagedResponse = new CollectingResponse(); + let impersonationSession: Awaited> | undefined; + let stage: "SESSION_CREATION" | "EXPIRY_REGISTRATION" | "SUCCESS_AUDIT" = "SESSION_CREATION"; + + try { + impersonationSession = await createNewSession(baseRequest, stagedResponse, tenantId, targetLoginMethod.recipeUserId, { + isImpersonation: true, + impersonatedBy: actorUserId, + impersonationExpiresAt: expiresAt, + }); + + stage = "EXPIRY_REGISTRATION"; + await registerImpersonationExpiry({ + sessionHandle: impersonationSession.getHandle(), + expiresAt, + }); + + stage = "SUCCESS_AUDIT"; + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + targetUserId: targetUser.id, + targetRecipeUserId, + tenantId, + reason, + outcome: "SESSION_CREATED", + impersonationSessionHandle: impersonationSession.getHandle(), + expiresAt, + }); + } catch { + let revoked: boolean | undefined; + if (impersonationSession) { + try { + revoked = await Session.revokeSession(impersonationSession.getHandle()); + } catch { + revoked = false; + } + } + + let failureOutcome = "SESSION_CREATION_FAILED"; + if (stage === "EXPIRY_REGISTRATION") { + failureOutcome = "EXPIRY_REGISTRATION_FAILED"; + } else if (stage === "SUCCESS_AUDIT") { + failureOutcome = "SUCCESS_AUDIT_FAILED"; + } + + await recordImpersonationAuditEvent({ + attemptId, + actorUserId, + targetUserId: targetUser.id, + targetRecipeUserId, + tenantId, + reason, + outcome: failureOutcome, + impersonationSessionHandle: impersonationSession?.getHandle(), + revoked, + revocationFailed: revoked === false, + expiresAt, + }); + return NextResponse.json({ message: "Impersonation request failed" }, { status: 500 }); + } + + // Staged credentials remain unreachable until every post-creation control succeeds. + copyCollectedCredentials(stagedResponse, baseResponse); + return NextResponse.json({ message: "Impersonation successful" }); + }, + ); +} +``` + + + + + + -:::info Multi Tenancy -Notice that the `"public"` `tenantId` goes to the function call above. This is the default `tenantId` in SuperTokens. If you are using the multi-tenancy feature and want to login into a different tenant, you can replace `"public"` with the `tenantId` you want to login into. +:::info[Multi Tenancy] +Most examples use the default `"public"` tenant. In a multi-tenant application, derive the target tenant from trusted server-side data and verify that the actor may impersonate users in that tenant. -You can fetch this `tenantId` based on the admin user's tenant (from their session), or you can pass it in the request body. +Never authorize a tenant only because its ID was supplied in the request body. If the client selects a tenant, treat that value as client-controlled input and check it against the actor's server-side tenant assignments before target lookup or session creation. ::: -- The API should call from your frontend application such that the frontend SDKs' network interceptors are running. -- In the API above, session verification runs first to ensure that the user has the admin role. If not, the API returns a `403` to the frontend. -- The target user is then fetched based on their email ID. If the user does not exist, an error occurs (which you can map to a `400` status code). -- A new session is then created using the target user's user ID. The `isImpersonation` flag goes as `true` in the access token payload to detect this on the frontend and show a message to the admin user that they are impersonating the target user (this is UI you would have to build if you want to). You can also use this custom access token payload to protect certain APIs which the admin cannot call, even if in impersonation mode. In the code, you use `isImpersonation`, but you can use anything else you like. In fact, you can even add the admin user's user ID to the access token payload (with a key like `adminUserId`), for logging purposes. -- The new session tokens attach to the response and overwrite the existing admin session. Cookies apply if the request contains the `st-auth-mode: "cookie"` header, otherwise the mode is header-based auth. Since you would be calling this API via the frontend interceptors, you do not need to explicitly set this header since the frontend SDK does this on its own. -- Once impersonated, the admin user can logout of the target user's session by clicking on the sign out button of your app - nothing special needs to happen there. +- The API should be called from your frontend application so that the frontend SDK's network interceptors run. +- In the APIs above, required-session verification and backend admin-role validation run before the request reads or looks up the target. A missing or invalid session is rejected, and a session without the required role is rejected with `403`. +- Prefer a stable target user or recipe-user ID. If you look up by account information instead, reject zero or multiple matches rather than selecting the first result. +- A new session is then created using the target user's user ID. The `isImpersonation` flag is added to the access token payload so that the frontend can show that the staff member is impersonating a user. Backend APIs can also use this claim to restrict actions while impersonating. Treat claims such as `isImpersonation` and `impersonatedBy` as enforcement context, not as a durable audit log. +- The new session tokens attach to the response and overwrite the active admin credentials in that browser. This does not revoke the original admin session in SuperTokens. Cookies apply if the request contains the `st-auth-mode: "cookie"` header; otherwise, the mode is header-based authentication. The frontend interceptors set this header automatically. +- Signing out revokes the current impersonation session. Also provide explicit early termination and enforce your maximum duration on the backend; do not depend on the user remembering to sign out. diff --git a/docs/post-authentication/session-management/introduction.mdx b/docs/post-authentication/session-management/introduction.mdx index 7280af3613..6288fb3de5 100644 --- a/docs/post-authentication/session-management/introduction.mdx +++ b/docs/post-authentication/session-management/introduction.mdx @@ -1,18 +1,21 @@ --- -title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true -description: >- - Learn about SuperTokens' session management and how to handle session security - and flow. -page_type: overview -recipe: session -category: session-management +title: Session Management +description: Understand access and refresh tokens, automatic session refresh, protected API verification, cookies, and session revocation. +sidebar: + label: Overview + order: 10 --- + -# Session management +## Session lifecycle summary + +- Signing in creates a session and issues access and refresh tokens to the frontend. +- Protected API calls verify the access token and its expiry. +- When an access token expires, the frontend uses the refresh token to obtain new tokens and retries the original request. +- Revoking a session removes its refresh token and session information, so refresh fails and the user must log in again. + + ## Overview @@ -20,7 +23,13 @@ SuperTokens provides session management out of the box. Sessions get created when a user signs in and maintained throughout the authentication lifecycle. In the next graphic you can see a high level overview of how the session flow works. -Flowcharts showing an overview of session flow + + Flowcharts showing an overview of session flow + - After sign in, the system creates a new session by issuing a refresh and access token to the frontend. - The frontend sends the access token for each API call that requires session authentication. @@ -44,88 +53,42 @@ In the next graphic you can see a high level overview of how the session flow wo ## Getting started Including the `Session` recipe in the initial configuration enables sessions. -This step is outlined in all the guides that show you how to integrate different authentication methods: [Email Password](/docs/authentication/email-password/introduction), [Passwordless](/docs/authentication/passwordless/introduction) or [Social Login](/docs/authentication/social/introduction). +This step is outlined in all the guides that show you how to integrate different authentication methods: [Email Password](/authentication/email-password/introduction), [Passwordless](/authentication/passwordless/introduction) or [Social Login](/authentication/social/introduction). Additionally, this section includes information on how to work with sessions after a user has signed in. - - - - Access Session Data - - - See how you can read the properties of an active session. - - - - - Invalidate Sessions - - - Learn how to revoke a session either through a user action or programmatically. - - - + + +See how you can read the properties of an active session. + + +Learn how to revoke a session either through a user action or programmatically. + + ## Customization - - - - - Share sessions across subdomains - - - Update the configuration to share sessions across different subdomains. - - - - - Switch between cookie and header based authentication - - - Choose how tokens are sent and stored during the authentication lifecycle. - - - - - Implement anonymous sessions - - - Learn how to track session data event if a user has not logged in. - - - - - Implement user impersonation - - - See how to act as a different user during the authentication flow. - - - - - Customize error handling - - - Change the default error handling behaviour. - - - - - Work with multiple API endpoints - - - Use the same session management logic for multiple API endpoints. - - - - - Blacklist Access Tokens - - - Block access tokens from being used. - - - + + +Update the configuration to share sessions across different subdomains. + + +Choose how tokens are sent and stored during the authentication lifecycle. + + +Learn how to track session data event if a user has not logged in. + + +See how to act as a different user during the authentication flow. + + +Change the default error handling behaviour. + + +Use the same session management logic for multiple API endpoints. + + +Block access tokens from being used. + + diff --git a/docs/post-authentication/session-management/meta.ts b/docs/post-authentication/session-management/meta.ts new file mode 100644 index 0000000000..53b352f860 --- /dev/null +++ b/docs/post-authentication/session-management/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Session Management", + icon: "id-card", + order: 1.1, +}); diff --git a/docs/post-authentication/session-management/security.mdx b/docs/post-authentication/session-management/security.mdx index 292615bea1..47eb57e994 100644 --- a/docs/post-authentication/session-management/security.mdx +++ b/docs/post-authentication/session-management/security.mdx @@ -1,19 +1,10 @@ --- title: Security -hide_title: true -sidebar_position: 5 -description: >- - Learn how to protect your website from CSRF attacks using the `sameSite` - cookie attribute. -page_type: guide -recipe: session -category: session-management +description: Learn how to protect your website from CSRF attacks using the `sameSite` cookie attribute. +sidebar: + order: 70 --- - - -# Session security - ## Overview The following page takes you through some common security considerations that the **SuperTokens** `Session` recipe handles. @@ -32,8 +23,7 @@ If a user visits a malicious site, requests from those sites do not have the ses ### Configure anti-csrf -:::caution -- SuperTokens automatically defends against CSRF attacks. +:::warning[- SuperTokens automatically defends against CSRF attacks.] - Please only change this setting if you know what you are doing. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). - This setting does not apply while using header-based authentication, since they get the same protection as `antiCsrf` set to `VIA_CUSTOM_HEADER`. ::: @@ -49,33 +39,30 @@ This sets automatically if `sameSite` is `none` or if your `apiDomain` and `webs Use this method if you want to allow any origin to query your APIs. This method may cause issues in browsers like Safari, especially if your site embeds as an `iframe`. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - antiCsrf: "VIA_CUSTOM_HEADER", // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + antiCsrf: "VIA_CUSTOM_HEADER", // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -84,44 +71,37 @@ import ( ) func main() { - // highlight-start // Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" antiCsrf := "VIA_CUSTOM_HEADER" - // highlight-end supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line AntiCsrf: &antiCsrf, }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start # Should be one of "NONE" or "VIA_CUSTOM_HEADER" or "VIA_TOKEN" anti_csrf='VIA_CUSTOM_HEADER' - # highlight-end ) ] ) ``` - - - + + --- @@ -129,7 +109,7 @@ init( [Per GDPR](https://gdpr.eu/cookies/), users do not need to give consent for your application to use session cookies. This is because they fall under essential cookies and not tracking cookies: -:::info Important +:::info[Important] "While it is not required to obtain consent for these cookies, explain to the user what they do and why they are necessary." ::: @@ -142,7 +122,7 @@ The ``sameSite`` cookie attribute declares if your cookies should restrict to a The ``sameSite`` attribute can have three possible values: - ``none`` - Cookies attach in all contexts, that is, cookies attach to both first-party and cross-origin requests. - - On Safari however, if third-party cookies do not work (which is the default behavior), and the website and `API` domains do not share the same top-level domain, then cookies do not go. Please check [the session management page](/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication) to see how you can switch to using headers. + - On Safari however, if third-party cookies do not work (which is the default behavior), and the website and `API` domains do not share the same top-level domain, then cookies do not go. Please check [the session management page](/post-authentication/session-management/switch-between-cookies-and-header-authentication) to see how you can switch to using headers. - ``lax`` - Cookies are only sent in a first-party context and along with `GET` requests initiated by third party websites (that result in browser navigation - user clicking on a link). - ``strict`` @@ -150,39 +130,34 @@ The ``sameSite`` attribute can have three possible values: ### Configuration -:::caution -- SuperTokens automatically sets the value of the ``sameSite`` cookie attribute based on your website and `API` domain configuration. +:::warning[- SuperTokens automatically sets the value of the ``sameSite`` cookie attribute based on your website and `API` domain configuration.] - Please only change this setting if you are a web security expert. If you are unsure, please feel free to [ask questions](https://supertokens.com/discord). ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - cookieSameSite: "strict", // Should be one of "strict" or "lax" or "none" - }), - ], + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + cookieSameSite: "strict", // Should be one of "strict" or "lax" or "none" + }), + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -191,45 +166,37 @@ import ( ) func main() { - // highlight-start // Should be one of "strict" or "lax" or "none" cookieSameSite := "lax" - // highlight-end supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line CookieSameSite: &cookieSameSite, }), }, }) } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start cookie_same_site='lax' # Should be one of 'strict' or 'lax' or 'none' - # highlight-end ) ] ) ``` - - - + + --- @@ -242,37 +209,33 @@ This, in turn, prevents cookie theft via man in the middle attacks. You can explicitly set the security level of cookies using the next snippet: -:::important -If not explicitly set, SuperTokens automatically determines the value for the `secure` attribute based on your API domain having `http` or `https`. +:::note[If not explicitly set, SuperTokens automatically determines the value for the `secure` attribute based on your API domain having `http` or `https`.] ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-next-line - cookieSecure: true, - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + cookieSecure: true, + }), + ], }); ``` - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -281,41 +244,35 @@ import ( ) func main() { - // highlight-next-line cookieSecure := true supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line CookieSecure: &cookieSecure, }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start cookie_secure=True - # highlight-end ) ] ) ``` - - - + + --- @@ -324,84 +281,67 @@ init( Access Token signing key rotation implies that the secret key for signing the access tokens changes at a fixed time interval. This reduces the risk of key theft. -:::info -- Existing logged in users are not logged out on key change. +:::info[- Existing logged in users are not logged out on key change.] - This feature enables by default. ::: ### Change the key rotation interval - - - + + ```bash docker run \ -p 3567:3567 \ - // highlight-start -e ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL=168 \ - // highlight-end -d supertokens/supertokens- ``` - - - - - + + ```yaml # You need to add the following to the config.yaml file. # The file path can be found by running the "supertokens --help" command access_token_dynamic_signing_key_update_interval: 168 - ``` - - + + - ``access_token_dynamic_signing_key_update_interval`` - Time in hours for how frequently the signing key changes. - It must have a ``number`` value with, the default value set to ``168`` -:::info -For managed service, update this value in the **Session Management** configuration card in the relevant deployment's **Configuration** page. +:::info[For managed service, update this value in the **Session Management** configuration card in the relevant deployment's **Configuration** page.] ::: ### Use static keys -If you do not want to use dynamic keys for session creation, then you can tell SuperTokens to use the static key instead. This is useful in cases where you want to [hard-code the public key for JWT verification in some process](/docs/additional-verification/session-verification/protect-api-routes#with-the-public-key-string). - - - +If you do not want to use dynamic keys for session creation, then you can tell SuperTokens to use the static key instead. This is useful in cases where you want to [hard-code the public key for JWT verification in some process](/additional-verification/session-verification/protect-api-routes#with-the-public-key-string). + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-next-line - useDynamicAccessTokenSigningKey: false, - }) - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + useDynamicAccessTokenSigningKey: false, + }), + ], }); ``` - -:::caution -Updating this value causes a spike in the session refresh API, as and when users visit your application. -::: - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -410,59 +350,47 @@ import ( ) func main() { - // highlight-next-line useDynamicAccessTokenSigningKey := false supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-next-line UseDynamicAccessTokenSigningKey: &useDynamicAccessTokenSigningKey, }), }, }) } ``` - -:::caution -Updating this value causes a spike in the session refresh API, as and when users visit your application. -::: - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start use_dynamic_access_token_signing_key=False - # highlight-end ) ] ) ``` + + -:::caution -Updating this value causes a spike in the session refresh API, as and when users visit your application. +:::warning[Updating this value causes a spike in the session refresh API, as and when users visit your application.] ::: - - - --- ## See also - - - - - - - + + + + + + + diff --git a/docs/post-authentication/session-management/session-invalidation.mdx b/docs/post-authentication/session-management/session-invalidation.mdx index f82d4cad61..1244758d5f 100644 --- a/docs/post-authentication/session-management/session-invalidation.mdx +++ b/docs/post-authentication/session-management/session-invalidation.mdx @@ -1,20 +1,12 @@ --- title: Session Invalidation -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 description: >- - Learn how to handle session expiry, implement sign out, and revoke sessions - across different programming languages and frameworks. -page_type: guide -recipe: session -category: session-management + Learn how to handle session expiry, implement sign out, and revoke sessions across different programming languages and + frameworks. +sidebar: + order: 30 --- - - -# Session invalidation - ## Overview You can invalidate a session in **SuperTokens** in different ways. @@ -24,7 +16,9 @@ This guide shows you how to implement each of these. ## Before you start - +:::info[Access token guidance] +This guide applies to scenarios involving **SuperTokens Session Access Tokens**. +::: --- @@ -32,138 +26,105 @@ This guide shows you how to implement each of these. The frontend SDK exposes a `signOut` function that revokes the session for the user. You need to add your own UI element for this since the library does not expose any components. -The `signOut` function calls the sign out API exposed by the session recipe on the backend and, in turn, revokes all the user active sessions. +The `signOut` function calls the sign out API exposed by the session recipe on the backend and revokes the current session. +It does not revoke the user's other sessions. Use the explicit all-session API shown below when that is the intended behavior. If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, the SDKs automatically perform a session refresh before revoking the session. -:::important -You have to add your own redirection logic after the sign out call completes. +:::note[You have to add your own redirection logic after the sign out call completes.] ::: - - - - - - + + + + ```tsx import React from "react"; -// highlight-next-line import { signOut } from "supertokens-auth-react/recipe/session"; function NavBar() { async function onLogout() { - // highlight-next-line await signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } return (
  • Home
  • - // highlight-next-line
  • Logout
  • -
- ) + ); } ``` -
- - - - + + ```tsx -// highlight-next-line import Session from "supertokens-web-js/recipe/session"; -async function logout () { - // highlight-next-line - await Session.signOut(); +async function logout() { + await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` + + - - - -
+ -
+ - - - - - - + + + + ```tsx import Session from "supertokens-web-js/recipe/session"; -async function logout () { - // highlight-next-line - await Session.signOut(); +async function logout() { + await Session.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -async function logout () { - // highlight-next-line - await supertokensSession.signOut(); + + +```tsx check=false reason="Requires SDK globals from surrounding application" +async function logout() { + await supertokensSession.signOut(); window.location.href = "/auth"; // or redirect to wherever the login page is } ``` - - - - - - - - - - - - + + + + + + ```tsx import SuperTokens from "supertokens-react-native"; -async function logout () { - // highlight-next-line - await SuperTokens.signOut(); +async function logout() { + await SuperTokens.signOut(); // navigate to the login screen.. } ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens class MainApplication: Application() { fun logout() { - // highlight-next-line SuperTokens.signOut(this); // navigate to the login screen.. } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -172,7 +133,7 @@ fileprivate class ViewController: UIViewController { func signOut() { SuperTokens.signOut(completionHandler: { error in - + if error != nil { // handle error } else { @@ -182,11 +143,8 @@ fileprivate class ViewController: UIViewController { } } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -198,27 +156,33 @@ Future signOut() async { ); } ``` + + + + - - - - - - - + ### Expose a backend sign out method If you do not want to use the frontend function you can expose a backend sign out method. - - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -226,19 +190,15 @@ import { SessionRequest } from "supertokens-node/framework/express"; let app = express(); -// highlight-start app.post("/someapi", verifySession(), async (req: SessionRequest, res) => { + // This will delete the session from the db and from the frontend (cookies) + await req.session!.revokeSession(); - // This will delete the session from the db and from the frontend (cookies) - await req.session!.revokeSession(); - // highlight-end - - res.send("Success! User session revoked"); + res.send("Success! User session revoked"); }); ``` - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -247,27 +207,24 @@ import { SessionRequest } from "supertokens-node/framework/hapi"; let server = Hapi.server({ port: 8000 }); server.route({ - path: "/someapi", - method: "post", - //highlight-start - options: { - pre: [ - { - method: verifySession() - }, - ], - }, - handler: async (req: SessionRequest, res) => { - // This will delete the session from the db and from the frontend (cookies) - await req.session!.revokeSession(); - // highlight-end - return res.response("Success! User session revoked").code(200); - } -}) + path: "/someapi", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + // This will delete the session from the db and from the frontend (cookies) + await req.session!.revokeSession(); + return res.response("Success! User session revoked").code(200); + }, +}); ``` - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -275,63 +232,55 @@ import { SessionRequest } from "supertokens-node/framework/fastify"; let fastify = Fastify(); -//highlight-start -fastify.post("/someapi", { +fastify.post( + "/someapi", + { preHandler: verifySession(), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { // This will delete the session from the db and from the frontend (cookies) await req.session!.revokeSession(); - // highlight-end res.send("Success! User session revoked"); -}); + }, +); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; -// highlight-start async function someapi(awsEvent: SessionEvent) { - // This will delete the session from the db and from the frontend (cookies) - await awsEvent.session!.revokeSession(); - // highlight-end + // This will delete the session from the db and from the frontend (cookies) + await awsEvent.session!.revokeSession(); - return { - body: JSON.stringify({ message: "Success! User session revoked" }), - statusCode: 200, - }; -}; + return { + body: JSON.stringify({ message: "Success! User session revoked" }), + statusCode: 200, + }; +} exports.handler = verifySession(someapi); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import { SessionContext } from 'supertokens-node/framework/koa'; +import { SessionContext } from "supertokens-node/framework/koa"; let router = new KoaRouter(); -//highlight-start router.post("/someapi", verifySession(), async (ctx: SessionContext, next) => { - // This will delete the session from the db and from the frontend (cookies) - await ctx.session!.revokeSession(); - // highlight-end + // This will delete the session from the db and from the frontend (cookies) + await ctx.session!.revokeSession(); - ctx.body = "Success! User session revoked"; + ctx.body = "Success! User session revoked"; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; @@ -339,94 +288,53 @@ import { verifySession } from "supertokens-node/recipe/session/framework/loopbac import { SessionContext } from "supertokens-node/framework/loopback"; class Logout { - //highlight-start - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) { } - @post("/someapi") - @intercept(verifySession()) - @response(200) - async handler() { - // This will delete the session from the db and from the frontend (cookies) - await this.ctx.session!.revokeSession(); - // highlight-end - - return "Success! User session revoked"; - } + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: SessionContext) {} + @post("/someapi") + @intercept(verifySession()) + @response(200) + async handler() { + // This will delete the session from the db and from the frontend (cookies) + await this.ctx.session!.revokeSession(); + + return "Success! User session revoked"; + } } ``` - - - - - - - - - + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; -// highlight-start export default async function someapi(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - // highlight-next-line - await verifySession()(req, res, next); - }, - req, - res - ) - // This will delete the session from the db and from the frontend (cookies) - await req.session!.revokeSession(); - // highlight-end - res.send("Success! User session revoked"); -} -``` - - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - // This will delete the session from the db and from the frontend (cookies) - await session!.revokeSession(); - return NextResponse.json({ message: "Success! User session revoked" }); - }); + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); + // This will delete the session from the db and from the frontend (cookies) + await req.session!.revokeSession(); + res.send("Success! User session revoked"); } ``` - - - - - -```ts + + +```ts check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Request, Response, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; +import { AuthGuard } from "./auth/auth.guard"; @Controller() export class ExampleController { // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. - @Post('someapi') + @Post("someapi") @UseGuards(new AuthGuard()) async postSomeAPI(@Session() session: SessionContainer): Promise { await session.revokeSession(); @@ -435,11 +343,10 @@ export class ExampleController { } } ``` - - - - - + + + + ```go import ( "net/http" @@ -465,49 +372,38 @@ func someAPI(w http.ResponseWriter, r *http.Request) { // TODO: Send 200 response to client } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.session.framework.fastapi import verify_session from supertokens_python.recipe.session import SessionContainer from fastapi import Depends from fastapi.responses import PlainTextResponse -# highlight-start async def some_api(session: SessionContainer = Depends(verify_session())): await session.revoke_session() # This will delete the session from the db and from the frontend (cookies) - # highlight-end return PlainTextResponse(content='success') ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session import SessionContainer from flask import g -# highlight-start -@app.route('/some_api', methods=['POST']) # type: ignore +@app.route('/some_api', methods=['POST']) @verify_session() def some_api(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens session.sync_revoke_session() # This will delete the session from the db and from the frontend (cookies) - # highlight-end return 'success' ``` - - - - -```python + + +```python check=false reason="Requires surrounding async application context" from typing import cast from django.http import HttpRequest @@ -516,41 +412,71 @@ from supertokens_python.recipe.session import SessionContainer from supertokens_python.recipe.session.framework.django.asyncio import verify_session -# highlight-start @verify_session() async def some_api(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore This will delete the session from the db and from the frontend (cookies) - # highlight-end + session: SessionContainer = cast(SessionContainer, request.supertokens) # Set by the session middleware. await session.revoke_session() ``` + + + + + + + + + + - - +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; - - +SuperTokens.init(backendConfig()); -:::info Tip +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + // This will delete the session from the db and from the frontend (cookies) + await session!.revokeSession(); + return NextResponse.json({ message: "Success! User session revoked" }); + }); +} +``` -If you are using the pre-built UI, and the `` component set custom post log out logic with the `onSessionExpired` prop. + + + + + + +:::info[Tip] + +If you are using the pre-built UI and the `` component, you can set custom post-logout logic with the `onSessionExpired` prop. The handler gets called if: - The backend has revoked the session, but not the frontend. - The user has been inactive for too long and their refresh token has expired. -```tsx +```tsx check=false reason="Requires surrounding framework application context" import React from "react"; import { SessionAuth } from "supertokens-auth-react/recipe/session"; -// @ts-ignore import MyComponent from "./myComponent"; const App = () => { - return ( - {/* ... */ }}> - - - ); -} + return ( + { + /* ... */ + }} + > + + + ); +}; ``` ::: @@ -560,33 +486,29 @@ const App = () => { To invalidate a session without relying on the intervention of a user you can create your own custom methods using the backend SDKs. -:::caution -This method of revoking a session only deletes the session from the database and not from the frontend. +:::warning[This method of revoking a session only deletes the session from the database and not from the frontend.] This implies that the user can still access protected endpoints while their access token is alive. -If you want to instantly logout the user in this mode, you should [enable access token blacklisting](/docs/post-authentication/session-management/advanced-workflows/access-token-blacklisting). +If you want to instantly logout the user in this mode, you should [enable access token blacklisting](/post-authentication/session-management/advanced-workflows/access-token-blacklisting). ::: ### Revoke a specific session - - - + + ```tsx import Session from "supertokens-node/recipe/session"; async function revokeSession(sessionHandle: string) { - let revoked = await Session.revokeSession(sessionHandle); -}; + let revoked = await Session.revokeSession(sessionHandle); +} ``` - - - + + ```go import "github.com/supertokens/supertokens-golang/recipe/session" func main() { sessionHandle := "someSessionHandle" - // highlight-next-line revoked, err := session.RevokeSession(sessionHandle) if err != nil { // TODO: Handle error @@ -600,45 +522,36 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.session.asyncio import revoke_session async def some_func(): session_handle = "someSessionHandle" - # highlight-next-line _ = await revoke_session(session_handle) ``` - - - - + + ```python from supertokens_python.recipe.session.syncio import revoke_session session_handle = "someSessionHandle" -# highlight-next-line revoked = revoke_session(session_handle) ``` + + + + - - - - - -You can fetch all the `sessionHandle`s for a user using the [`getAllSessionHandlesForUser` function](/docs/post-authentication/session-management/access-session-data#fetch-all-user-sessions) +You can fetch all the `sessionHandle`s for a user using the [`getAllSessionHandlesForUser` function](/post-authentication/session-management/access-session-data#fetch-all-user-sessions) ### Revoke all sessions for a user - - - + + ```tsx import express from "express"; import Session from "supertokens-node/recipe/session"; @@ -646,17 +559,14 @@ import Session from "supertokens-node/recipe/session"; let app = express(); app.use("/revoke-all-user-sessions", async (req, res) => { + let userId = req.body.userId; + await Session.revokeAllSessionsForUser(userId); - let userId = req.body.userId - // highlight-next-line - await Session.revokeAllSessionsForUser(userId); - - res.send("Success! All user sessions have been revoked"); + res.send("Success! All user sessions have been revoked"); }); ``` - - - + + ```go import ( "fmt" @@ -665,7 +575,6 @@ import ( ) func main() { - // highlight-next-line tenantId := "public" revokedSessionHandles, err := session.RevokeAllSessionsForUser("userId", &tenantId) if err != nil { @@ -677,45 +586,35 @@ func main() { fmt.Println(revokedSessionHandles) } ``` - - - - - - - - + + + + ```python from supertokens_python.recipe.session.asyncio import revoke_all_sessions_for_user async def some_func(): user_id = "someUserId" - # highlight-next-line revoked_session_handles = await revoke_all_sessions_for_user(user_id) print(revoked_session_handles) # revoked_session_handles is an array of revoked session handles. ``` - - - - + + ```python from supertokens_python.recipe.session.syncio import revoke_all_sessions_for_user user_id = "someUserId" -# highlight-next-line revoked_session_handles = revoke_all_sessions_for_user(user_id) # revoked_session_handles is an array of revoked session handles. ``` + + + + - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] By default, revokeAllSessionsForUser deletes all the sessions for the user across all the tenants. If you want to delete the sessions for a user in a specific tenant, you can pass the tenant ID as a parameter to the function call. ::: @@ -723,11 +622,10 @@ By default, revokeAllSessionsForUser deletes all the sessions for the user acros ## See also - - - - - - - - + + + + + + + diff --git a/docs/post-authentication/session-management/share-session-across-sub-domains.mdx b/docs/post-authentication/session-management/share-session-across-sub-domains.mdx index b2dfc39020..4107fa3316 100644 --- a/docs/post-authentication/session-management/share-session-across-sub-domains.mdx +++ b/docs/post-authentication/session-management/share-session-across-sub-domains.mdx @@ -1,22 +1,15 @@ --- title: Share sessions across sub domains -hide_title: true -sidebar_position: 3 description: Configure SuperTokens to share sessions across multiple subdomains. -page_type: guide -recipe: session -category: session-management +sidebar: + order: 40 --- - -# Share sessions across subdomains - - ## Overview Configure sharing sessions across multiple subdomains in SuperTokens by setting the `sessionTokenFrontendDomain` attribute of the Session recipe in your frontend code. -:::info Example +:::info[Example] - Your app has two subdomains `abc.example.com` and `xyz.example.com`. Assume that the user logs in via `example.com` - To enable sharing sessions across `example.com`, `abc.example.com` and `xyz.example.com`, set the `sessionTokenFrontendDomain` attribute to `.example.com`. ::: @@ -27,95 +20,93 @@ Configure sharing sessions across multiple subdomains in SuperTokens by setting ### 1. Update the frontend configuration - + - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - // ... - // this should be equal to the domain where the user will see the login UI - apiDomain: "...", - appName: "...", - websiteDomain: "https://example.com" - }, - recipeList: [ - Session.init({ - // highlight-next-line - sessionTokenFrontendDomain: ".example.com" - }) - ] + appInfo: { + // ... + // this should be equal to the domain where the user will see the login UI + apiDomain: "...", + appName: "...", + websiteDomain: "https://example.com", + }, + recipeList: [ + Session.init({ + sessionTokenFrontendDomain: ".example.com", + }), + ], }); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - + + +```tsx check=false reason="Partial configuration example" supertokensUIInit({ - appInfo: { - // ... - // this should be equal to the domain where the user will see the login UI - apiDomain: "...", - appName: "...", - websiteDomain: "https://example.com" - }, - recipeList: [ - supertokensUISession.init({ - // highlight-next-line - sessionTokenFrontendDomain: ".example.com" - }) - ] + appInfo: { + // ... + // this should be equal to the domain where the user will see the login UI + apiDomain: "...", + appName: "...", + websiteDomain: "https://example.com", + }, + recipeList: [ + supertokensUISession.init({ + sessionTokenFrontendDomain: ".example.com", + }), + ], }); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - sessionTokenFrontendDomain: ".example.com" - // highlight-end - }), - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + sessionTokenFrontendDomain: ".example.com", + }), + ], +}); ``` + + - - - - -:::caution +:::warning - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: -:::info Multi Tenancy +:::info[Multi Tenancy] If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. @@ -125,66 +116,72 @@ To solve this, add extra information about access token payload containing a lis ::: - + - + - - - - + + +:::warning[Not applicable] +::: + + + + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - sessionTokenFrontendDomain: ".example.com" - // highlight-end - }), - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + sessionTokenFrontendDomain: ".example.com", + }), + ], +}); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - supertokensSession.init({ - // ... - // highlight-start - sessionTokenFrontendDomain: ".example.com" - // highlight-end - }) - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + supertokensSession.init({ + // ... + sessionTokenFrontendDomain: ".example.com", + }), + ], +}); ``` + + + + - - + + + + + +:::warning -:::caution - Do not set `sessionTokenFrontendDomain` to a value that's in the [public suffix list](https://publicsuffix.org/list/public_suffix_list.dat) (Search for your value without the leading dot). Otherwise, session management does not work. - Do not set `sessionTokenFrontendDomain` to `.localhost` or an IP address based domain with a leading `.` since browsers reject these cookies. For local development, you should configure [your machine to use alias for `localhost`](https://superuser.com/questions/152146/how-to-alias-a-hostname-on-mac-osx). ::: -:::info Multi Tenancy +:::info[Multi Tenancy] If each tenant belongs to one subdomain, and a user has access to more than one tenant, the tenant ID in the session is always the one from which they logged in. @@ -193,31 +190,23 @@ For example, if a user has access to tenant `t1.example.com` and `t2.example.com To solve this, add extra information about access token payload containing a list of all the tenants that the user has access to. Then read from that list instead of the `tId` claim. ::: + + - - - - -:::caution -Not applicable -::: - - - - + --- ## See also - - - - - - - - + + + + + + + + diff --git a/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication.mdx b/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication.mdx index 638ae94310..a96299b297 100644 --- a/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication.mdx +++ b/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication.mdx @@ -1,25 +1,16 @@ --- title: Switch between cookie and header-based sessions -hide_title: true -sidebar_position: 4 -description: >- - Switch between cookie and header-based sessions for secure token management in - SuperTokens. -page_type: guide -recipe: session -category: session-management +description: Switch between cookie and header-based sessions for secure token management in SuperTokens. +sidebar: + order: 50 --- - -# Switch between cookie and header-based sessions - - ## Overview SuperTokens supports 2 methods of authorizing requests. The following guide shows you how to switch between them. -### Cookie based +### Cookie based - The default in the web SDKs - Uses [`HttpOnly` cookies](https://owasp.org/www-community/HttpOnly) by default to prevent token theft via XSS @@ -35,8 +26,7 @@ The backend controls this choice, but it follows a preference set in the fronten ## Before you start -:::caution -We recommend cookie-based sessions in browsers because header-based sessions require saving the access and refresh tokens in storage vulnerable to XSS attacks. +:::warning[We recommend cookie-based sessions in browsers because header-based sessions require saving the access and refresh tokens in storage vulnerable to XSS attacks.] ::: ## Steps @@ -46,159 +36,152 @@ We recommend cookie-based sessions in browsers because header-based sessions req You can provide a `tokenTransferMethod` property in the configuration of the Session recipe to set the preferred token transfer method. The backend receives this method with every request in the `st-auth-mode` header. By default, the backend follows this preference. - + - + - + + +You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - +This change is in your auth route configuration. + + + + ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-next-line - tokenTransferMethod: "header" // or "cookie" - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", // or "cookie" + }), + ], }); ``` - - - - - -You need to make changes to the auth route configuration, as well as to the `supertokens-web-js` SDK configuration at the root of your application: - -This change is in your auth route configuration. - -```tsx + + +```tsx check=false reason="Requires SDK globals from surrounding application" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUISession.init({ - // highlight-next-line - tokenTransferMethod: "header" // or "cookie" - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUISession.init({ + tokenTransferMethod: "header", // or "cookie" + }), + ], }); ``` + + + + This change goes in the `supertokens-web-js` SDK configuration at the root of your application: + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-next-line - tokenTransferMethod: "header" // or "cookie" - }) - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", // or "cookie" + }), + ], +}); ``` + + - - - + - + - - - - - - + + + + +You can use the `tokenTransferMethod` builder method to set what mode the SDK should use for sessions. + + + + + + + + + ```tsx import SuperTokens from "supertokens-web-js"; import Session from "supertokens-web-js/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - Session.init({ - // highlight-next-line - tokenTransferMethod: "header" // or "cookie" - }) - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + Session.init({ + tokenTransferMethod: "header", // or "cookie" + }), + ], +}); ``` - - - - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; + + +```tsx check=false reason="Requires SDK globals from surrounding application" supertokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - }, - recipeList: [ - supertokensSession.init({ - // highlight-next-line - tokenTransferMethod: "header" // or "cookie", - }) - ], -}) + appInfo: { + apiDomain: "...", + appName: "...", + }, + recipeList: [ + supertokensSession.init({ + tokenTransferMethod: "header", // or "cookie", + }), + ], +}); ``` - - - - - - - - - - - - + + + + + + ```tsx -import SuperTokens from 'supertokens-react-native'; +import SuperTokens from "supertokens-react-native"; SuperTokens.init({ - apiDomain: "...", - tokenTransferMethod: "header", // or "cookie". "header" by default + apiDomain: "...", + tokenTransferMethod: "header", // or "cookie". "header" by default }); ``` - - - - - -You can use the `tokenTransferMethod` builder method to set what mode the SDK should use for sessions. - + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -206,20 +189,73 @@ import com.supertokens.session.SuperTokens class MainApplication: Application() { override fun onCreate() { super.onCreate() - + SuperTokens.Builder(this, "...") .tokenTransferMethod("header") // or "cookie". "header" by default .build() } } ``` + + +```swift +import UIKit +import SuperTokensIOS + +fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + try SuperTokens.initialize( + apiDomain: "...", + tokenTransferMethod: .header // or .cookie . header by default + ) + } catch SuperTokensError.initError(let message) { + // TODO: Handle initialization error + } catch { + // Some other error + } + return true + } + +} +``` + + +```dart +import 'package:supertokens_flutter/supertokens.dart'; + +void main() { + SuperTokens.init( + apiDomain: "...", + tokenTransferMethod: SuperTokensTokenTransferMethod.COOKIE, + ); +} +``` + + + + + + + + + ### Using cookies When using cookies for session management you need to enable cookies before making requests. #### With `HttpURLConnection` - + + + + + + + + + ```kotlin import android.app.Application import com.supertokens.session.SuperTokens @@ -235,11 +271,27 @@ class MainApplication: Application() { } } ``` - + + + + + + + + + `SuperTokensPersistentCookieStore` is a cookie store that SuperTokens provides which uses SharedPreferences to persist sessions across app launches #### With `OkHttp` / `Retrofit` - + + + + + + + + + ```kotlin import android.content.Context import com.franmontiel.persistentcookiejar.PersistentCookieJar @@ -271,97 +323,55 @@ class NetworkManager { } } ``` - + + + + + + + + + In the above example, `PersistentCookieJar` from `'com.github.franmontiel:PersistentCookieJar:v1.0.1'` enables persistently storing cookies using SharedPreferences to maintain sessions across app launches. + + + + - - - - -```swift -import UIKit -import SuperTokensIOS - -fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - do { - try SuperTokens.initialize( - apiDomain: "...", - tokenTransferMethod: .header // or .cookie . header by default - ) - } catch SuperTokensError.initError(let message) { - // TODO: Handle initialization error - } catch { - // Some other error - } - - return true - } - -} -``` - - - - - -```dart -import 'package:supertokens_flutter/supertokens.dart'; - -void main() { - SuperTokens.init( - apiDomain: "...", - tokenTransferMethod: SuperTokensTokenTransferMethod.COOKIE, - ); -} -``` - - - - - - + - - -### 2. Update the backend configuration {{optional}} +### 2. Update the backend configuration (optional) This step is optional. You can force the backend to use a specific token transfer method regardless of the frontend configuration. -:::caution -**You should not set this on the backend if you have more than one client using different modes** (for example if you have a website that uses cookie based, and a mobile app that uses header based sessions). +:::warning[**You should not set this on the backend if you have more than one client using different modes** (for example if you have a website that uses cookie based, and a mobile app that uses header based sessions).] ::: - - - + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-start - getTokenTransferMethod: () => "header", - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + getTokenTransferMethod: () => "header", + }), + ], }); ``` - - - + + ```go import ( "net/http" @@ -374,20 +384,17 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - // highlight-start GetTokenTransferMethod: func(req *http.Request, forCreateNewSession bool, userContext supertokens.UserContext) sessmodels.TokenTransferMethod { return sessmodels.HeaderTransferMethod }, - // highlight-end }), }, }) } ``` - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import init, InputAppInfo from supertokens_python.recipe import session from supertokens_python.framework import BaseRequest @@ -400,20 +407,16 @@ def get_token_transfer_method(req: BaseRequest, for_create_new_session: bool, us init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore + framework='...', recipe_list=[ session.init( - # highlight-start get_token_transfer_method=get_token_transfer_method - # highlight-end ) ] ) ``` + + - - - -:::note -By default, session verification allows both cookie and authorization bearer tokens. When creating a new session, it follows the preference of the frontend indicated by the `st-auth-mode` request header (set by the frontend SDK). +:::note[By default, session verification allows both cookie and authorization bearer tokens. When creating a new session, it follows the preference of the frontend indicated by the `st-auth-mode` request header (set by the frontend SDK).] ::: diff --git a/docs/post-authentication/user-management/_category_.json b/docs/post-authentication/user-management/_category_.json deleted file mode 100644 index a9432697ca..0000000000 --- a/docs/post-authentication/user-management/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "User Management", - "position": 2 -} diff --git a/docs/post-authentication/user-management/account-deduplication.mdx b/docs/post-authentication/user-management/account-deduplication.mdx index c07c2ad2b5..9a161ff635 100644 --- a/docs/post-authentication/user-management/account-deduplication.mdx +++ b/docs/post-authentication/user-management/account-deduplication.mdx @@ -1,18 +1,10 @@ --- title: Account deduplication -hide_title: true -sidebar_position: 5 -description: >- - Learn how to prevent duplicate user accounts through account deduplication - techniques. -page_type: guide -recipe: emailpassword -category: authentication +description: Learn how to prevent duplicate user accounts through account deduplication techniques. +sidebar: + order: 60 --- - -# Account deduplication - ## Overview Users may forget the initial method they used to sign up and may create multiple accounts with the same email ID - leading to a poor user experience. @@ -35,102 +27,109 @@ The difference is that whilst deduplication prevents duplicate sign ups, account The approach to implementing account deduplication is to override the backend functions / APIs. This way, you can check if a user already exists and return an error to the frontend if the condition is true. - - - -```tsx -import ThirdParty from "supertokens-node/recipe/thirdparty" + + +```tsx check=false reason="Requires surrounding application context" +import ThirdParty from "supertokens-node/recipe/thirdparty"; import Passwordless from "supertokens-node/recipe/passwordless"; import supertokens from "supertokens-node"; let recipeList = [ - Passwordless.init({ - contactMethod: "EMAIL", // REMOVE_FROM_OUTPUT - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", // REMOVE_FROM_OUTPUT - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - createCodePOST: async function (input) { - if ("email" in input) { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - email: input.email - }); - if (existingUsers.length === 0) { - // this means this email is new so we allow sign up - return originalImplementation.createCodePOST!(input); - } - if (existingUsers.find(u => - u.loginMethods.find(lM => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== undefined)) { - // this means that the existing user is a passwordless login user. So we allow it - return originalImplementation.createCodePOST!(input); - } - return { - status: "GENERAL_ERROR", - message: "Seems like you already have an account with another method. Please use that instead." - } - } - // phone number based login, so we allow it. - return originalImplementation.createCodePOST!(input); - }, - } + Passwordless.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + createCodePOST: async function (input) { + if ("email" in input) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email, + }); + if (existingUsers.length === 0) { + // this means this email is new so we allow sign up + return originalImplementation.createCodePOST!(input); + } + if ( + existingUsers.find( + (u) => + u.loginMethods.find((lM) => lM.hasSameEmailAs(input.email) && lM.recipeId === "passwordless") !== + undefined, + ) + ) { + // this means that the existing user is a passwordless login user. So we allow it + return originalImplementation.createCodePOST!(input); + } + return { + status: "GENERAL_ERROR", + message: "Seems like you already have an account with another method. Please use that instead.", + }; } - } - }), - ThirdParty.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signInUp: async function (input) { - let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { - email: input.email - }); - if (existingUsers.length === 0) { - // this means this email is new so we allow sign up - return originalImplementation.signInUp(input); - } - if (existingUsers.find(u => - u.loginMethods.find(lM => lM.hasSameThirdPartyInfoAs({ - id: input.thirdPartyId, - userId: input.thirdPartyUserId - }) && lM.recipeId === "thirdparty") !== undefined)) { - // this means we are trying to sign in with the same social login. So we allow it - return originalImplementation.signInUp(input); - } - // this means that the email already exists with another social or passwordless login method, so we throw an error. - throw new Error("Cannot sign up as email already exists"); - } - } - }, - apis: (originalImplementation) => { + // phone number based login, so we allow it. + return originalImplementation.createCodePOST!(input); + }, + }; + }, + }, + }), + ThirdParty.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signInUp: async function (input) { + let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, { + email: input.email, + }); + if (existingUsers.length === 0) { + // this means this email is new so we allow sign up + return originalImplementation.signInUp(input); + } + if ( + existingUsers.find( + (u) => + u.loginMethods.find( + (lM) => + lM.hasSameThirdPartyInfoAs({ + id: input.thirdPartyId, + userId: input.thirdPartyUserId, + }) && lM.recipeId === "thirdparty", + ) !== undefined, + ) + ) { + // this means we are trying to sign in with the same social login. So we allow it + return originalImplementation.signInUp(input); + } + // this means that the email already exists with another social or passwordless login method, so we throw an error. + throw new Error("Cannot sign up as email already exists"); + }, + }; + }, + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInUpPOST: async function (input) { + try { + return await originalImplementation.signInUpPOST!(input); + } catch (err: any) { + if (err.message === "Cannot sign up as email already exists") { + // this error was thrown from our function override above. + // so we send a useful message to the user return { - ...originalImplementation, - signInUpPOST: async function (input) { - try { - return await originalImplementation.signInUpPOST!(input); - } catch (err: any) { - if (err.message === "Cannot sign up as email already exists") { - // this error was thrown from our function override above. - // so we send a useful message to the user - return { - status: "GENERAL_ERROR", - message: "Seems like you already have an account with another method. Please use that instead." - } - } - throw err; - } - } - } + status: "GENERAL_ERROR", + message: "Seems like you already have an account with another method. Please use that instead.", + }; + } + throw err; } - } - }) -] + }, + }; + }, + }, + }), +]; ``` - - - - + + ```go import ( "errors" @@ -243,11 +242,9 @@ func main() { } } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from typing import Any, Dict, Optional, Union from supertokens_python import InputAppInfo, init @@ -447,11 +444,11 @@ def override_passwordless_apis(original_implementation: PasswordlessAPIInterface init( app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="...", recipe_list=[ passwordless.init( - contact_config=..., # type: ignore - flow_type="...", # type: ignore + contact_config=..., + flow_type="...", override=passwordless.InputOverrideConfig( apis=override_passwordless_apis, ), @@ -464,9 +461,8 @@ init( ], ) ``` - - - + + In the above code snippet, override the `signInUpPOST` (third party recipe) and the `createCodePOST` (passwordless recipe) API as well as the `signInUp` recipe function. @@ -480,7 +476,7 @@ The `signInUp` recipe function is overridden to: - If instead, a user exists, but has the same `thirdPartyId` and `thirdPartyUserId`, implying that this is a sign in (for example a user who had signed up with Google is signing in with Google), the operation proceeds by calling the `originalImplementation` function. - If neither of the conditions above match, it means that the user is trying to sign up with a third party provider whilst they already have an account with another provider or via passwordless login. Here, the system throws an error with some custom message. -Finally, the `signInUpPOST` API is overridden to catch that custom error and return a [general error status](/docs/references/backend-sdks/api-overrides#error-management) to the frontend with a message displayed to the user in the sign in form. +Finally, the `signInUpPOST` API is overridden to catch that custom error and return a [general error status](/references/backend-sdks/api-overrides#error-management) to the frontend with a message displayed to the user in the sign in form. The `createCodePOST` API is also overridden to perform similar checks: - If the input is phone number based, then the system calls the `originalImplementation` function allowing sign up or sign in. This is OK since social login is always email based, there is no scope of duplication. @@ -489,7 +485,7 @@ The `createCodePOST` API is also overridden to perform similar checks: - Else, check if the existing user is not a Third Party login user, implying that it's a Passwordless login user. Here, the `originalImplementation` function is also called to allow the user to sign in. - If neither of the conditions above match, it means that the user is trying to sign up with passwordless login whilst they already have an account with a third party provider. Here, the system returns an appropriate message to display on the frontend. -:::info Multi Tenancy +:::info[Multi Tenancy] For a multi tenant setup, the customisations above ensure that multiple accounts with the same email don't exist within a single tenant. To ensure no duplication across all tenants, when fetching the list of existing users, loop through all tenants in the app. You can fetch them by using the `listAllTenants` function of the multi tenancy recipe. diff --git a/docs/post-authentication/user-management/allow-users-to-update-their-data.mdx b/docs/post-authentication/user-management/allow-users-to-update-their-data.mdx index 3a4c098f24..5d3619ebc3 100644 --- a/docs/post-authentication/user-management/allow-users-to-update-their-data.mdx +++ b/docs/post-authentication/user-management/allow-users-to-update-their-data.mdx @@ -1,27 +1,17 @@ --- title: Allow users to update their data -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 description: Enable users to change their email or password. -page_type: guide -recipe: emailpassword -category: authentication +sidebar: + order: 30 --- - - -# Allow users to change their data - - ## Overview This guide shows you how to implement a feature that allows users to update their email or password. ## Before you start -:::caution -SuperTokens does not provide the UI for this type of use case. +:::warning[SuperTokens does not provide the UI for this type of use case.] You need to create the UI and set up a route on your backend to have this functionality. ::: @@ -40,30 +30,23 @@ In this flow, a user can update their account's email without verifying the new #### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. -- To learn more about how to use the session verification middleware for other frameworks, click [this link](/docs/additional-verification/session-verification/protect-api-routes) - - - - +- To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes) + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); -// highlight-start app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { - // TODO: see next steps -}) - -// highlight-end + // TODO: see next steps +}); ``` - - - - + + ```go import ( "net/http" @@ -71,7 +54,6 @@ import ( "github.com/supertokens/supertokens-golang/recipe/session" ) -// highlight-start // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -84,15 +66,10 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } -// highlight-end ``` - - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask @@ -100,84 +77,71 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -# highlight-start -@app.route('/change-email', methods=['POST']) # type: ignore +@app.route('/change-email', methods=['POST']) @verify_session() def change_email(): pass # TODO: see next steps -# highlight-end ``` - - - - + + #### 2. Update the account - Validate the input email. - Update the account with the input email. - - - + + ```tsx // the following example uses express import Passwordless from "supertokens-node/recipe/passwordless"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { - - // highlight-start - let session = req.session!; - let email = req.body.email; - - // Validate the input email - if (!isValidEmail(email)) { - // TODO: handle invalid email error - return - } - - // Update the email - let resp = await Passwordless.updateUser({ - recipeUserId: session.getRecipeUserId(), - email: email - }) - - if (resp.status === "OK") { - // TODO: send successfully updated email response - return - } - if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { - // TODO: handle error that email exists with another account. - return - } - if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { - // This is possible if you have enabled account linking. - // See our docs for account linking to know more about this. - // TODO: tell the user to contact support. - } - throw new Error("Should never come here"); - // highlight-end - -}) + let session = req.session!; + let email = req.body.email; + + // Validate the input email + if (!isValidEmail(email)) { + // TODO: handle invalid email error + return; + } + + // Update the email + let resp = await Passwordless.updateUser({ + recipeUserId: session.getRecipeUserId(), + email: email, + }); + + if (resp.status === "OK") { + // TODO: send successfully updated email response + return; + } + if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // TODO: handle error that email exists with another account. + return; + } + if (resp.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { + // This is possible if you have enabled account linking. + // See our docs for account linking to know more about this. + // TODO: tell the user to contact support. + } + throw new Error("Should never come here"); +}); function isValidEmail(email: string) { - let regexp = new RegExp( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ); - return regexp.test(email); + let regexp = new RegExp( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ); + return regexp.test(email); } ``` - - - - - - + + ```go import ( "encoding/json" @@ -202,7 +166,6 @@ func main() { } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(r.Context()) var requestBody RequestBody @@ -236,7 +199,6 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } log.Fatal("should not reach here") - // highlight-end } @@ -248,12 +210,9 @@ func isValidEmail(email string) bool { return emailCheck } ``` - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from re import fullmatch from flask import Flask, g, request @@ -270,15 +229,14 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -@app.route("/change-email", methods=["POST"]) # type: ignore +@app.route("/change-email", methods=["POST"]) @verify_session() def change_email(): - # highlight-start session: SessionContainer = g.supertokens request_body = request.get_json() - email = str(request_body["email"]) # type: ignore + email = str(request_body["email"]) if request_body is None: # TODO: handle invalid body error return @@ -307,8 +265,6 @@ def change_email(): raise Exception("Should never reach here") - # highlight-end - def is_valid_email(value: str) -> bool: return ( @@ -319,41 +275,33 @@ def is_valid_email(value: str) -> bool: is not None ) ``` - - - + + ### With email verification In this flow, the user's account updates once they have verified the new email. -#### 1. Create the email update endpoint +#### 1. Create the email update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. -- To learn more about how to use the session verification middleware for other frameworks, click [this link](/docs/additional-verification/session-verification/protect-api-routes) - - - - +- To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes) + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); -// highlight-start app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { - // TODO: see next steps -}) - -// highlight-end + // TODO: see next steps +}); ``` - - - - + + ```go import ( "net/http" @@ -361,7 +309,6 @@ import ( "github.com/supertokens/supertokens-golang/recipe/session" ) -// highlight-start // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -374,15 +321,10 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } -// highlight-end ``` - - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask @@ -390,17 +332,13 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -# highlight-start -@app.route('/change-email', methods=['POST']) # type: ignore +@app.route('/change-email', methods=['POST']) @verify_session() def change_password(): pass # TODO: see next steps -# highlight-end ``` - - - - + + #### 2. Initiate the email verification flow @@ -410,100 +348,96 @@ def change_password(): - If the email is **NOT** verified, create and send the verification email. - If the email has been verified, update the account with the new email. - - - + + ```tsx import Passwordless from "supertokens-node/recipe/passwordless"; import EmailVerification from "supertokens-node/recipe/emailverification"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; import supertokens from "supertokens-node"; -import {isEmailChangeAllowed} from "supertokens-node/recipe/accountlinking" +import { isEmailChangeAllowed } from "supertokens-node/recipe/accountlinking"; let app = express(); app.post("/change-email", verifySession(), async (req: SessionRequest, res: express.Response) => { - - // highlight-start - let session = req.session!; - let email = req.body.email; - - // validate the input email - if (!isValidEmail(email)) { - return res.status(400).send("Email is invalid"); + let session = req.session!; + let email = req.body.email; + + // validate the input email + if (!isValidEmail(email)) { + return res.status(400).send("Email is invalid"); + } + + // Then, we check if the email is verified for this user ID or not. + // It is important to understand that SuperTokens stores email verification + // status based on the user ID AND the email, and not just the email. + let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); + + if (!isVerified) { + if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) { + // this can come here if you have enabled the account linking feature, and + // if there is a security risk in changing this user's email. + return res.status(400).send("Email change not allowed. Please contact support"); } - - // Then, we check if the email is verified for this user ID or not. - // It is important to understand that SuperTokens stores email verification - // status based on the user ID AND the email, and not just the email. - let isVerified = await EmailVerification.isEmailVerified(session.getRecipeUserId(), email); - - if (!isVerified) { - if (!(await isEmailChangeAllowed(session.getRecipeUserId(), email, false))) { - // this can come here if you have enabled the account linking feature, and - // if there is a security risk in changing this user's email. - return res.status(400).send("Email change not allowed. Please contact support"); - } - // Before sending a verification email, we check if the email is already - // being used by another user. If it is, we throw an error. - let user = (await supertokens.getUser(session.getUserId()))!; - for (let i = 0; i < user?.tenantIds.length; i++) { - // Since once user can be shared across many tenants, we need to check if - // the email already exists in any of the tenants. - let usersWithEmail = await supertokens.listUsersByAccountInfo(user?.tenantIds[i], { - email - }) - for (let y = 0; y < usersWithEmail.length; y++) { - if (usersWithEmail[y].id !== session.getUserId()) { - // TODO handle error, email already exists with another user. - return - } - } + // Before sending a verification email, we check if the email is already + // being used by another user. If it is, we throw an error. + let user = (await supertokens.getUser(session.getUserId()))!; + for (let i = 0; i < user?.tenantIds.length; i++) { + // Since once user can be shared across many tenants, we need to check if + // the email already exists in any of the tenants. + let usersWithEmail = await supertokens.listUsersByAccountInfo(user?.tenantIds[i], { + email, + }); + for (let y = 0; y < usersWithEmail.length; y++) { + if (usersWithEmail[y].id !== session.getUserId()) { + // TODO handle error, email already exists with another user. + return; } - - // Now we create and send the email verification link to the user for the new email. - await EmailVerification.sendEmailVerificationEmail(session.getTenantId(), session.getUserId(), session.getRecipeUserId(), email); - - // TODO send successful response that email verification email sent. - return + } } - // Since the email is verified, we try and do an update - let resp = await Passwordless.updateUser({ - recipeUserId: session.getRecipeUserId(), - email: email, - }); - - if (resp.status === "OK") { - // TODO send successful response that email updated. - return - } - if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { - // TODO handle error, email already exists with another user. - return - } - - throw new Error("Should never come here"); - // highlight-end + // Now we create and send the email verification link to the user for the new email. + await EmailVerification.sendEmailVerificationEmail( + session.getTenantId(), + session.getUserId(), + session.getRecipeUserId(), + email, + ); -}) + // TODO send successful response that email verification email sent. + return; + } + + // Since the email is verified, we try and do an update + let resp = await Passwordless.updateUser({ + recipeUserId: session.getRecipeUserId(), + email: email, + }); + + if (resp.status === "OK") { + // TODO send successful response that email updated. + return; + } + if (resp.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // TODO handle error, email already exists with another user. + return; + } + + throw new Error("Should never come here"); +}); function isValidEmail(email: string) { - let regexp = new RegExp( - /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ - ); - return regexp.test(email); + let regexp = new RegExp( + /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, + ); + return regexp.test(email); } ``` - - - - - - + + ```go import ( @@ -530,7 +464,6 @@ func main() { } func changeEmailAPI(w http.ResponseWriter, r *http.Request) { - // highlight-start sessionContainer := session.GetSessionFromRequestContext(r.Context()) var requestBody RequestBody @@ -588,7 +521,6 @@ func changeEmailAPI(w http.ResponseWriter, r *http.Request) { } log.Fatal("should not reach here") - // highlight-end } @@ -601,12 +533,9 @@ func isValidEmail(email string) bool { } ``` - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from re import fullmatch from flask import Flask, g, request @@ -629,11 +558,10 @@ from supertokens_python.types.base import AccountInfoInput app = Flask(__name__) -@app.route("/change-email", methods=["POST"]) # type: ignore +@app.route("/change-email", methods=["POST"]) @verify_session() def change_email(): - # highlight-start - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens request_body = request.get_json() @@ -699,7 +627,6 @@ def change_email(): # TODO handle error, email already exists return - # highlight-end raise Exception("Should never reach here") @@ -713,11 +640,10 @@ def is_valid_email(value: str) -> bool: is not None ) ``` + + - - - -:::info Multi Tenancy +:::info[Multi Tenancy] - Notice that the process loops through all the tenants that this user belongs to check that for each of the tenants, there is no other user with the new email. If this step is not done, then calling `updateEmailOrPassword` would fail because the email is already used by another user in one of the tenants that this user belongs to. In that case, the verification process should not proceed either. - The `tenantId` of the current session is also passed when calling the `sendEmailVerificationEmail` function, ensuring that the link generated opens the tenant's UI that the user interacts with. @@ -729,9 +655,8 @@ def is_valid_email(value: str) -> bool: - Update the accounts email on successful email verification. - - - + + ```tsx import SuperTokens from "supertokens-node"; import Passwordless from "supertokens-node/recipe/passwordless"; @@ -739,50 +664,44 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - Passwordless.init({ - flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", - contactMethod: "EMAIL_OR_PHONE" - }), - EmailVerification.init({ - mode: "REQUIRED", - override: { - apis: (oI) => { - return { - ...oI, - verifyEmailPOST: async function (input) { - // highlight-start - let response = await oI.verifyEmailPOST!(input); - if (response.status === "OK") { - // This will update the email of the user to the one - // that was just marked as verified by the token. - await Passwordless.updateUser({ - recipeUserId: response.user.recipeUserId, - email: response.user.email, - }); - } - return response; - // highlight-end - }, - }; - }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Passwordless.init({ + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + contactMethod: "EMAIL_OR_PHONE", + }), + EmailVerification.init({ + mode: "REQUIRED", + override: { + apis: (oI) => { + return { + ...oI, + verifyEmailPOST: async function (input) { + let response = await oI.verifyEmailPOST!(input); + if (response.status === "OK") { + // This will update the email of the user to the one + // that was just marked as verified by the token. + await Passwordless.updateUser({ + recipeUserId: response.user.recipeUserId, + email: response.user.email, + }); + } + return response; }, - }), - Session.init(), - ], + }; + }, + }, + }), + Session.init(), + ], }); ``` - - - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailverification" @@ -816,7 +735,6 @@ func main() { originalVerifyEmailPOST := *originalImplementation.VerifyEmailPOST (*originalImplementation.VerifyEmailPOST) = func(token string, sessionContainer sessmodels.SessionContainer, tenantId string, options evmodels.APIOptions, userContext supertokens.UserContext) (evmodels.VerifyEmailPOSTResponse, error) { - // highlight-start response, err := originalVerifyEmailPOST(token, sessionContainer, tenantId, options, userContext) if response.OK != nil { // This will update the email of the user to the one @@ -826,7 +744,6 @@ func main() { // TODO: Handle error } } - // highlight-end return response, err } @@ -845,11 +762,8 @@ func main() { } ``` - - - - - + + ```python from typing import Any, Dict, Optional @@ -881,7 +795,6 @@ def override_email_verification_apis(original_implementation: APIInterface): api_options: APIOptions, user_context: Dict[str, Any], ): - # highlight-start verification_response = await original_email_verification_verify_email_post( token, session, tenant_id, api_options, user_context ) @@ -893,7 +806,6 @@ def override_email_verification_apis(original_implementation: APIInterface): ) return verification_response - # highlight-end original_implementation.email_verify_post = email_verify_post return original_implementation @@ -911,21 +823,18 @@ init( emailverification.init( "REQUIRED", override=emailverification.InputOverrideConfig( - # highlight-start apis=override_email_verification_apis - # highlight-end ), ), ], ) ``` - - - + + --- -## Password update +## Password update This section has instructions on how to create a route, on your backend, that can update a user's password. Calling this route checks if the old password is valid and updates the user's profile with the new password. @@ -933,30 +842,23 @@ Calling this route checks if the old password is valid and updates the user's pr ### 1. Create the password update endpoint - You need to create a route on the backend protected by the session verification middleware, ensuring that only an authenticated user can access the protected route. -- To learn more about how to use the session verification middleware for other frameworks, click [this link](/docs/additional-verification/session-verification/protect-api-routes#using-verify-session) - - - - +- To learn more about how to use the session verification middleware for other frameworks, click [this link](/additional-verification/session-verification/protect-api-routes#using-verify-session) + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); -// highlight-start app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { - // TODO: see next steps -}) - -// highlight-end + // TODO: see next steps +}); ``` - - - - + + ```go import ( "net/http" @@ -964,7 +866,6 @@ import ( "github.com/supertokens/supertokens-golang/recipe/session" ) -// highlight-start // the following example uses net/http func main() { _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -977,14 +878,10 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { // TODO: see next steps } -// highlight-end ``` - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" # the following example uses flask from flask import Flask @@ -992,93 +889,83 @@ from supertokens_python.recipe.session.framework.flask import verify_session app = Flask(__name__) -# highlight-start -@app.route('/change-password', methods=['POST']) # type: ignore +@app.route('/change-password', methods=['POST']) @verify_session() def change_password(): pass # TODO: see next steps -# highlight-end ``` - - - - + + ### 2. Update the user password - The `session` object can be used to retrieve the logged-in user's `userId`. - Use the recipe's sign in function and check if the old password is valid -- Update the user's password. - - - +- Update the user's password. + + ```tsx // the following example uses express import EmailPassword from "supertokens-node/recipe/emailpassword"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; import supertokens from "supertokens-node"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { -// highlight-start - // get the supertokens session object from the req - let session = req.session - - // retrieve the old password from the request body - let oldPassword = req.body.oldPassword + // get the supertokens session object from the req + let session = req.session; - // retrieve the new password from the request body - let updatedPassword = req.body.newPassword + // retrieve the old password from the request body + let oldPassword = req.body.oldPassword; - // get the signed in user's email from the getUserById function - let userInfo = await supertokens.getUser(session!.getUserId()) + // retrieve the new password from the request body + let updatedPassword = req.body.newPassword; - if (userInfo === undefined) { - throw new Error("Should never come here") - } - - let loginMethod = userInfo.loginMethods.find((lM) => lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword"); - if (loginMethod === undefined) { - throw new Error("Should never come here") - } - const email = loginMethod.email!; - - // call signin to check that input password is correct - let isPasswordValid = await EmailPassword.verifyCredentials(session!.getTenantId(), email, oldPassword) - - if (isPasswordValid.status !== "OK") { - // TODO: handle incorrect password error - return - } - - - // update the user's password using updateEmailOrPassword - let response = await EmailPassword.updateEmailOrPassword({ - recipeUserId: session!.getRecipeUserId(), - password: updatedPassword, - tenantIdForPasswordPolicy: session!.getTenantId() - }) + // get the signed in user's email from the getUserById function + let userInfo = await supertokens.getUser(session!.getUserId()); - if (response.status === "PASSWORD_POLICY_VIOLATED_ERROR") { - // TODO: handle incorrect password error - return - } - - // TODO: send successful password update response - // highlight-end - -}) + if (userInfo === undefined) { + throw new Error("Should never come here"); + } + let loginMethod = userInfo.loginMethods.find( + (lM) => + lM.recipeUserId.getAsString() === session!.getRecipeUserId().getAsString() && lM.recipeId === "emailpassword", + ); + if (loginMethod === undefined) { + throw new Error("Should never come here"); + } + const email = loginMethod.email!; + + // call signin to check that input password is correct + let isPasswordValid = await EmailPassword.verifyCredentials(session!.getTenantId(), email, oldPassword); + + if (isPasswordValid.status !== "OK") { + // TODO: handle incorrect password error + return; + } + + // update the user's password using updateEmailOrPassword + let response = await EmailPassword.updateEmailOrPassword({ + recipeUserId: session!.getRecipeUserId(), + password: updatedPassword, + tenantIdForPasswordPolicy: session!.getTenantId(), + }); + + if (response.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + // TODO: handle incorrect password error + return; + } + + // TODO: send successful password update response +}); ``` - - - - - + + ```go import ( "encoding/json" @@ -1130,7 +1017,6 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { return } - // highlight-start if isPasswordValid.WrongCredentialsError != nil { // TODO: Handle error return @@ -1149,16 +1035,12 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { return } // TODO: send successful password update response - // highlight-end } ``` - - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import g, request from supertokens_python.recipe.emailpassword.interfaces import ( @@ -1174,12 +1056,11 @@ from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.syncio import get_user -@app.route("/change-password", methods=["POST"]) # type: ignore +@app.route("/change-password", methods=["POST"]) @verify_session() def change_password(): - # highlight-start - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens # get the signed in user's email from the getUserById function @@ -1234,63 +1115,53 @@ def change_password(): return # TODO: send successful password update response - # highlight-end ``` + + - - - -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that the `tenantId` passes as an argument to the `signIn` and the `updateEmailOrPassword` functions. This ensures that the current tenant has email password enabled, and ensures that the user's new password matches the password policy defined for their tenant (if different password policies exist for different tenants). If this user shares access across multiple tenants, their password changes for all tenants. ::: -### 3. Revoke all sessions associated with the user {{optional}} +### 3. Revoke all sessions associated with the user (optional) - Revoking all sessions associated with the user forces them to re-authenticate with their new password. - - - + + ```tsx // the following example uses express import Session from "supertokens-node/recipe/session"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express" +import { SessionRequest } from "supertokens-node/framework/express"; import express from "express"; let app = express(); app.post("/change-password", verifySession(), async (req: SessionRequest, res: express.Response) => { + let userId = req.session!.getUserId(); - let userId = req.session!.getUserId(); - - /** - * - * ... - * see previous step - * ... - * - * */ - - // highlight-start - // revoke all sessions for the user - await Session.revokeAllSessionsForUser(userId) + /** + * + * ... + * see previous step + * ... + * + * */ - // revoke the current user's session, this removes the auth cookies, logging out the user on the frontend. - await req.session!.revokeSession() - //highlight-end + // revoke all sessions for the user + await Session.revokeAllSessionsForUser(userId); - // TODO: send successful password update response - -}) + // revoke the current user's session, this removes the auth cookies, logging out the user on the frontend. + await req.session!.revokeSession(); + // TODO: send successful password update response +}); ``` - - - - + + ```go import ( "net/http" @@ -1324,7 +1195,6 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { * * */ - // highlight-start // revoke all sessions for the user _, err := session.RevokeAllSessionsForUser(userID, nil) @@ -1338,16 +1208,13 @@ func changePasswordAPI(w http.ResponseWriter, r *http.Request) { // TODO: Handle error } - // highlight-end // TODO: send successful password update response } ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from typing import cast from flask import Flask @@ -1357,26 +1224,23 @@ from supertokens_python.recipe.session.framework.flask import verify_session from supertokens_python.recipe.session.syncio import revoke_all_sessions_for_user app = Flask(__name__) -@app.route('/change-password', methods=['POST']) # type: ignore +@app.route('/change-password', methods=['POST']) @verify_session() def change_password(): - session: SessionContainer = cast(SessionContainer, g.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, g.supertokens) # get the userId from the session object user_id = session.get_user_id() # TODO: see previous step... - # highlight-start # revoke all sessions for the user revoke_all_sessions_for_user(user_id) # revoke the user's current session, this removes the auth cookies, logging out the user on the frontend session.sync_revoke_session() - # highlight-end # TODO: send successful password update response ``` - - - + + diff --git a/docs/post-authentication/user-management/common-actions.mdx b/docs/post-authentication/user-management/common-actions.mdx index 7270b1cae5..dac14b5366 100644 --- a/docs/post-authentication/user-management/common-actions.mdx +++ b/docs/post-authentication/user-management/common-actions.mdx @@ -1,61 +1,48 @@ --- title: Common actions -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 2 description: Discover how to manage users using SuperTokens. -page_type: guide -recipe: emailpassword -category: authentication +sidebar: + order: 20 --- - -# User management actions - ## Overview -**SuperTokens** exposes a set of functions and APIs that you can use to have manual control over your users. +**SuperTokens** exposes a set of functions and APIs that you can use to have manual control over your users. Actions like fetching users or deleting them are available through different SDK calls. --- -## Get user +## Get user ### By email - - - - + + ```tsx import supertokens from "supertokens-node"; async function getUserInfo() { - let usersInfo = await supertokens.listUsersByAccountInfo("public", { - email: "test@example.com" - }); - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + email: "test@example.com", + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` - - - - - - + + ```go import ( "fmt" @@ -75,15 +62,10 @@ func main() { //... } ``` - - - - - - - - - + + + + ```python from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @@ -106,11 +88,8 @@ async def some_func(): # - information about if the user's email is verified or not. # ``` - - - - - + + ```python from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @@ -133,52 +112,46 @@ def some_func(): # - information about if the user's email is verified or not. # ``` + + + + - - - - - - -:::info Multi Tenancy -Notice that the first argument of the above function is `"public"`. This is the default `tenantId`, which means that SuperTokens returns information about the user whose email is `"test@example.com"` in the `"public"` tenant. +:::info[Multi Tenancy] +Notice that the first argument of the above function is `"public"`. This is the default `tenantId`, which means that SuperTokens returns information about the user whose email is `"test@example.com"` in the `"public"` tenant. If you are using the multi-tenancy feature, you can pass in a different `tenantId` to get information about a user in a different tenant. ::: ### By phone number - - - + + ```tsx import supertokens from "supertokens-node"; async function handler() { - let usersInfo = await supertokens.listUsersByAccountInfo("public", { - phoneNumber: "+1234567890" - }); - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ + let usersInfo = await supertokens.listUsersByAccountInfo("public", { + phoneNumber: "+1234567890", + }); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` - - - - - + + ```go import ( "fmt" @@ -198,14 +171,10 @@ func main() { //... } ``` - - - - - - - - + + + + ```python from supertokens_python.asyncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @@ -216,10 +185,8 @@ async def some_func(): "public", AccountInfoInput(phone_number="+1234567890") ) ``` - - - - + + ```python from supertokens_python.syncio import list_users_by_account_info from supertokens_python.types.base import AccountInfoInput @@ -230,39 +197,85 @@ def some_func(): "public", AccountInfoInput(phone_number="+1234567890") ) ``` + + + + - - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that the `"public"` `tenantId` appears in the function call above. This is the default `tenantId` and returns the user with the given phone number that belongs to the `public` tenant. You can provide a different `tenantId` if required. ::: ### By User ID - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from 'supertokens-node/framework/express'; +import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; let app = express(); app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + + let userInfo = await supertokens.getUser(userId); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ +}); +``` + + +```tsx +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import Hapi from "@hapi/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; +import supertokens from "supertokens-node"; + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/get-user-info", + method: "get", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) + + let userInfo = await supertokens.getUser(userId); /** - * + * * userInfo contains the following info: * - emails * - id @@ -272,129 +285,78 @@ app.get("/get-user-info", verifySession(), async (req: SessionRequest, res) => { * - third party login info * - all the login methods associated with this user. * - information about if the user's email is verified or not. - * - */ -}) -``` - - - - -```tsx -import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import Hapi from "@hapi/hapi"; -import { SessionRequest } from "supertokens-node/framework/hapi"; -import supertokens from "supertokens-node"; - -let server = Hapi.server({ port: 8000 }); - -server.route({ - path: "/get-user-info", - method: "get", - options: { - pre: [ - { - method: verifySession() - }, - ], - }, - // @ts-ignore - handler: async (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ - } -}) + * + */ + }, +}); ``` - - - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; -import { SessionRequest } from 'supertokens-node/framework/fastify'; +import { SessionRequest } from "supertokens-node/framework/fastify"; import supertokens from "supertokens-node"; const fastify = Fastify(); -fastify.post("/like-comment", { +fastify.post( + "/like-comment", + { preHandler: verifySession(), -}, async (req: SessionRequest, res) => { + }, + async (req: SessionRequest, res) => { let userId = req.session!.getUserId(); - // highlight-next-line - let userInfo = await supertokens.getUser(userId) + let userInfo = await supertokens.getUser(userId); /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ -}); + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + }, +); ``` - - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; import supertokens from "supertokens-node"; async function getUserInfo(awsEvent: SessionEvent) { - let userId = awsEvent.session!.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ -}; + let userId = awsEvent.session!.getUserId(); + + let userInfo = await supertokens.getUser(userId); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ +} exports.handler = verifySession(getUserInfo); ``` - - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -404,31 +366,27 @@ import supertokens from "supertokens-node"; let router = new KoaRouter(); router.get("/get-user-info", verifySession(), async (ctx: SessionContext, next) => { - let userId = ctx.session!.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ + let userId = ctx.session!.getUserId(); + + let userInfo = await supertokens.getUser(userId); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ }); ``` - - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, MiddlewareContext, get, response } from "@loopback/rest"; @@ -438,171 +396,116 @@ import { SessionContext } from "supertokens-node/framework/loopback"; import supertokens from "supertokens-node"; class GetUserInfo { - constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} - @get("/get-user-info") - @intercept(verifySession()) - @response(200) - async handler() { - let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ - } -} -``` - - - - + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @get("/get-user-info") + @intercept(verifySession()) + @response(200) + async handler() { + let userId = ((this.ctx as any).session as Session.SessionContainer).getUserId(); - - - + let userInfo = await supertokens.getUser(userId); + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + } +} +``` + + ```tsx -import { superTokensNextWrapper } from 'supertokens-node/nextjs' +import { superTokensNextWrapper } from "supertokens-node/nextjs"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; export default async function likeComment(req: SessionRequest, res: any) { - await superTokensNextWrapper( - async (next) => { - await verifySession()(req, res, next); - }, - req, - res - ) - - let userId = req.session!.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ -} -``` - - - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const userId = session!.getUserId(); - - // highlight-next-line - let userInfo = await SuperTokens.getUser(userId) - - /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ - return NextResponse.json({}); - }); + await superTokensNextWrapper( + async (next) => { + await verifySession()(req, res, next); + }, + req, + res, + ); + + let userId = req.session!.getUserId(); + + let userInfo = await supertokens.getUser(userId); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ } ``` - - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Request, Response } from "@nestjs/common"; -// @ts-ignore -import { AuthGuard } from './auth/auth.guard'; -// @ts-ignore -import { Session } from './auth/session.decorator'; +import { AuthGuard } from "./auth/auth.guard"; +import { Session } from "./auth/session.decorator"; import { SessionRequest } from "supertokens-node/framework/express"; import supertokens from "supertokens-node"; @Controller() export class ExampleController { - @Post('example') + @Post("example") @UseGuards(new AuthGuard()) // For more information about this guard please read our NestJS guide. - async postExample(@Request() req: SessionRequest, @Session() session: Session, @Response({passthrough: true}) res: Response): Promise { + async postExample( + @Request() req: SessionRequest, + @Session() session: Session, + @Response({ passthrough: true }) res: Response, + ): Promise { let userId = session.getUserId(); - - // highlight-next-line - let userInfo = await supertokens.getUser(userId) + + let userInfo = await supertokens.getUser(userId); /** - * - * userInfo contains the following info: - * - emails - * - id - * - timeJoined - * - tenantIds - * - phone numbers - * - third party login info - * - all the login methods associated with this user. - * - information about if the user's email is verified or not. - * - */ + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ return true; } } ``` - - - - - - - - - - + + + + + + ```go import ( "fmt" @@ -623,7 +526,6 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - // highlight-next-line userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error @@ -632,11 +534,8 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userInfo) } ``` - - - - - + + ```go import ( "fmt" @@ -670,7 +569,6 @@ func getUserInfoAPI(c *gin.Context) { userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - // highlight-next-line userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error @@ -680,11 +578,8 @@ func getUserInfoAPI(c *gin.Context) { //... } ``` - - - - - + + ```go import ( "fmt" @@ -705,7 +600,6 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - // highlight-next-line userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error @@ -714,11 +608,8 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userInfo) } ``` - - - - - + + ```go import ( "fmt" @@ -739,7 +630,6 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { userID := sessionContainer.GetUserID() // You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - // highlight-next-line userInfo, err := emailpassword.GetUserByID(userID) if err != nil { // TODO: Handle error @@ -748,17 +638,12 @@ func getUserInfoAPI(w http.ResponseWriter, r *http.Request) { fmt.Println(userInfo) } ``` - - - - - - - - - - - + + + + + + ```python from fastapi import Depends, FastAPI @@ -768,19 +653,16 @@ from supertokens_python.recipe.session.framework.fastapi import verify_session app = FastAPI() -@app.post('/get_user_info_api') # type: ignore +@app.post('/get_user_info_api') async def get_user_info_api(session: SessionContainer = Depends(verify_session())): user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - # highlight-next-line _ = await get_user(user_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding framework application context" from flask import Flask, g from supertokens_python.recipe.session import SessionContainer @@ -789,22 +671,19 @@ from supertokens_python.syncio import get_user app = Flask(__name__) -@app.route('/get_user_info', methods=['GET']) # type: ignore +@app.route('/get_user_info', methods=['GET']) @verify_session() def get_user_info_api(): - session: SessionContainer = g.supertokens # type: ignore + session: SessionContainer = g.supertokens user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - # highlight-next-line _ = get_user(user_id) ``` - - - - -```python + + +```python check=false reason="Requires surrounding async application context" from typing import cast from django.http import HttpRequest @@ -816,29 +695,74 @@ from supertokens_python.recipe.session.framework.django.asyncio import verify_se @verify_session() async def get_user_info_api(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore + session: SessionContainer = cast(SessionContainer, request.supertokens) user_id = session.get_user_id() # You can learn more about the `User` object over here https://github.com/supertokens/core-driver-interface/wiki - # highlight-next-line _ = await get_user(user_id) ``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const userId = session!.getUserId(); + + let userInfo = await SuperTokens.getUser(userId); + + /** + * + * userInfo contains the following info: + * - emails + * - id + * - timeJoined + * - tenantIds + * - phone numbers + * - third party login info + * - all the login methods associated with this user. + * - information about if the user's email is verified or not. + * + */ + return NextResponse.json({}); + }); +} +``` - - - - + + + + + -:::info Tip -The authentication session also contains the user ID and the session payload. -You can access it on [both the backend and the frontend](/docs/additional-verification/session-verification/claim-validation#using-the-access-token-payload). +:::info[Tip] +The authentication session also contains the user ID and the session payload. +You can access it on [both the backend and the frontend](/additional-verification/session-verification/claim-validation#using-the-access-token-payload). ::: #### Using the user metadata recipe -Checkout the [user metadata recipe docs](/docs/post-authentication/user-management/user-metadata) which shows you how to save and fetch any JSON object against the user's ID. You can use this to save information like the user's name (`first_name` and `last_name`) or any other field associated with the user. +Checkout the [user metadata recipe docs](/post-authentication/user-management/user-metadata) which shows you how to save and fetch any JSON object against the user's ID. You can use this to save information like the user's name (`first_name` and `last_name`) or any other field associated with the user. --- @@ -846,21 +770,18 @@ Checkout the [user metadata recipe docs](/docs/post-authentication/user-manageme ## Delete user - - - + + ```tsx -import {deleteUser} from "supertokens-node"; +import { deleteUser } from "supertokens-node"; async function deleteUserForId() { - let userId = "..." // get the user ID - await deleteUser(userId); // this will succeed even if the userId didn't exist. + let userId = "..."; // get the user ID + await deleteUser(userId); // this will succeed even if the userId didn't exist. } ``` - - - - + + ```go import "github.com/supertokens/supertokens-golang/supertokens" @@ -869,13 +790,10 @@ func main() { supertokens.DeleteUser(userId) // this will succeed even if the userId didn't exist. } ``` - - - - - - - + + + + ```python from supertokens_python.asyncio import delete_user @@ -884,10 +802,8 @@ async def do_delete(): user_id = "..." # get the user ID somehow... await delete_user(user_id) # this will succeed even if the userId didn't exist. ``` - - - - + + ```python from supertokens_python.syncio import delete_user @@ -895,16 +811,16 @@ from supertokens_python.syncio import delete_user user_id = "..." # get the user ID somehow... delete_user(user_id) # this will succeed even if the userId didn't exist. ``` - - - - - - - -:::caution -- Calling this function permanently removes all information associated with this user, including their sessions. -- If this user has an active access token, and you have not enabled access token blacklisting, session verification still succeeds until their access token expires. After that, the system logs them out since session refresh fails. + + + + + +:::warning[- Calling this function permanently removes all information associated with this user, including their sessions.] +- Deletion removes the user's database sessions, but it does not immediately invalidate an already-issued stateless access + token. Without `checkDatabase: true`, that token can continue to pass session verification until it expires. Enable an + authoritative database check on every endpoint that must reject the deleted user immediately. After the access token + expires, refresh fails because the database session no longer exists. ::: @@ -914,48 +830,45 @@ delete_user(user_id) # this will succeed even if the userId didn't exist. ### Newest first - - - + + ```tsx import { getUsersNewestFirst } from "supertokens-node"; async function getUsers() { - // get the latest 100 users - let usersResponse = await getUsersNewestFirst({ - tenantId: "public" - }); - - let users = usersResponse.users; - let nextPaginationToken = usersResponse.nextPaginationToken; - - // get the next 200 users - usersResponse = await getUsersNewestFirst({ - tenantId: "public", - limit: 200, - paginationToken: nextPaginationToken, - }) - - users = usersResponse.users; - nextPaginationToken = usersResponse.nextPaginationToken; - - // get for specific recipes - usersResponse = await getUsersNewestFirst({ - tenantId: "public", - limit: 200, - paginationToken: nextPaginationToken, - // only get for those users who signed up with ^{recipeNameCapitalLetters} - includeRecipeIds: ["^{rid}"], - }) - - users = usersResponse.users; - nextPaginationToken = usersResponse.nextPaginationToken; + // get the latest 100 users + let usersResponse = await getUsersNewestFirst({ + tenantId: "public", + }); + + let users = usersResponse.users; + let nextPaginationToken = usersResponse.nextPaginationToken; + + // get the next 200 users + usersResponse = await getUsersNewestFirst({ + tenantId: "public", + limit: 200, + paginationToken: nextPaginationToken, + }); + + users = usersResponse.users; + nextPaginationToken = usersResponse.nextPaginationToken; + + // get for specific recipes + usersResponse = await getUsersNewestFirst({ + tenantId: "public", + limit: 200, + paginationToken: nextPaginationToken, + // only get for those users who signed up with + includeRecipeIds: [""], + }); + + users = usersResponse.users; + nextPaginationToken = usersResponse.nextPaginationToken; } ``` - - - - + + ```go import "github.com/supertokens/supertokens-golang/supertokens" @@ -976,7 +889,7 @@ func main() { } // get for specific recipes - includeRecipeIds := []string{"^{rid}"} + includeRecipeIds := []string{""} result, err = supertokens.GetUsersNewestFirst("", result.NextPaginationToken, &limit, &includeRecipeIds, nil) if err != nil { // TODO: Handle error @@ -984,13 +897,10 @@ func main() { } } ``` - - - - - - - + + + + ```python from supertokens_python.asyncio import get_users_newest_first @@ -1007,14 +917,12 @@ async def some_func(): "public", 200, users_response.next_pagination_token, - # only get for those users who signed up with ^{recipeNameCapitalLetters} - ["^{rid}"] + # only get for those users who signed up with + [""] ) ``` - - - - + + ```python from supertokens_python.syncio import get_users_newest_first @@ -1030,66 +938,56 @@ users_response = get_users_newest_first( "public", 200, users_response.next_pagination_token, - # only get for those users who signed up with ^{recipeNameCapitalLetters} - ["^{rid}"] + # only get for those users who signed up with + [""] ) ``` + + + + - - - - - - -### Oldest first - - - +### Oldest first + + ```ts import { getUsersOldestFirst } from "supertokens-node"; async function getUsers() { - // get the latest 100 users - let usersResponse = await getUsersOldestFirst({ - tenantId: "public" - }); - let users = usersResponse.users; - let nextPaginationToken = usersResponse.nextPaginationToken; - - // get the next oldest 200 users - usersResponse = await getUsersOldestFirst({ - tenantId: "public", - limit: 200, - paginationToken: nextPaginationToken, - }); - - users = usersResponse.users; - nextPaginationToken = usersResponse.nextPaginationToken; - - // get for specific recipes - usersResponse = await getUsersOldestFirst({ - tenantId: "public", - limit: 200, - paginationToken: nextPaginationToken, - // only get for those users who signed up with ^{recipeNameCapitalLetters} - includeRecipeIds: ["^{rid}"] - }); - - users = usersResponse.users; - nextPaginationToken = usersResponse.nextPaginationToken; + // get the latest 100 users + let usersResponse = await getUsersOldestFirst({ + tenantId: "public", + }); + let users = usersResponse.users; + let nextPaginationToken = usersResponse.nextPaginationToken; + + // get the next oldest 200 users + usersResponse = await getUsersOldestFirst({ + tenantId: "public", + limit: 200, + paginationToken: nextPaginationToken, + }); + + users = usersResponse.users; + nextPaginationToken = usersResponse.nextPaginationToken; + + // get for specific recipes + usersResponse = await getUsersOldestFirst({ + tenantId: "public", + limit: 200, + paginationToken: nextPaginationToken, + // only get for those users who signed up with + includeRecipeIds: [""], + }); + + users = usersResponse.users; + nextPaginationToken = usersResponse.nextPaginationToken; } ``` - -- If the `nextPaginationToken` is `undefined`, then there are no more users to loop through. -- If there are no users in your app, then `nextPaginationToken` is `undefined` and `users` is an empty array -- Each element in the `users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). - - - - - + + ```go import "github.com/supertokens/supertokens-golang/supertokens" @@ -1110,7 +1008,7 @@ func main() { } // get for specific recipes - includeRecipeIds := []string{"^{rid}"} + includeRecipeIds := []string{""} result, err = supertokens.GetUsersOldestFirst("", result.NextPaginationToken, &limit, &includeRecipeIds, nil) if err != nil { // TODO: Handle error @@ -1118,18 +1016,10 @@ func main() { } } ``` - -- If the `result.NextPaginationToken` is `nil`, then there are no more users to loop through. -- If there are no users in your app, then `result.NextPaginationToken` is `nil` and `result.Users` is an empty array -- Each element in the `result.Users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). - - - - - - - - + + + + ```python from supertokens_python.asyncio import get_users_oldest_first @@ -1146,14 +1036,12 @@ async def some_func(): "public", 200, users_response.next_pagination_token, - # only get for those users who signed up with ^{recipeNameCapitalLetters} - ["^{rid}"] + # only get for those users who signed up with + [""] ) ``` - - - - + + ```python from supertokens_python.syncio import get_users_oldest_first @@ -1169,19 +1057,30 @@ users_response = get_users_oldest_first( "public", 200, users_response.next_pagination_token, - # only get for those users who signed up with ^{recipeNameCapitalLetters} - ["^{rid}"] + # only get for those users who signed up with + [""] ) ``` + + + + - - - - - + + +- If the `nextPaginationToken` is `undefined`, then there are no more users to loop through. +- If there are no users in your app, then `nextPaginationToken` is `undefined` and `users` is an empty array +- Each element in the `users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). + + +- If the `result.NextPaginationToken` is `nil`, then there are no more users to loop through. +- If there are no users in your app, then `result.NextPaginationToken` is `nil` and `result.Users` is an empty array +- Each element in the `result.Users` array is according to the output of the core API as shown in the [API documentation](https://app.swaggerhub.com/apis/supertokens/CDI/2.8.0#/Core/getUsers). + + -:::info Multi Tenancy +:::info[Multi Tenancy] Notice that the `tenantId` appears as `"public"`. This means that the functions above loop through the users of the `public` `tenantId`. If you want to loop through other tenant IDs, you can pass in the tenant ID string to the function call. This also implies that there is no way to loop through all users across all tenants in one go. If you want to do this, you must loop through each tenant one by one. @@ -1189,22 +1088,19 @@ This also implies that there is no way to loop through all users across all tena --- -## Count users - - - +## Count users + + ```ts -import {getUserCount} from "supertokens-node"; +import { getUserCount } from "supertokens-node"; async function getCount() { - let count = await getUserCount() + let count = await getUserCount(); } ``` - - - - + + ```go import ( "fmt" @@ -1223,12 +1119,10 @@ func main() { fmt.Println(count) } ``` - - - - - - + + + + ```python from supertokens_python.asyncio import get_user_count @@ -1238,10 +1132,8 @@ async def some_func(): print(user_count) # TODO.. ``` - - - - + + ```python from supertokens_python.syncio import get_user_count @@ -1249,12 +1141,11 @@ from supertokens_python.syncio import get_user_count user_count = get_user_count() ``` + + + + - - - - - -:::info Multi Tenancy +:::info[Multi Tenancy] By default, the getUserCount function returns the number of users across all tenants. If you want to get the number of users for a specific tenant, you can pass in the tenant ID string to the function call. ::: diff --git a/docs/post-authentication/user-management/introduction.mdx b/docs/post-authentication/user-management/introduction.mdx index 78ee36ee9b..7cafa34f41 100644 --- a/docs/post-authentication/user-management/introduction.mdx +++ b/docs/post-authentication/user-management/introduction.mdx @@ -1,17 +1,10 @@ --- title: Introduction -hide_title: true -sidebar_position: 1 -skip_llms_txt: true description: 'Learn how to manage users in SuperTokens ' -page_type: overview -recipe: emailpassword -category: authentication +sidebar: + order: 10 --- - -# User management - ## Overview Users are at the core of every authentication flow. @@ -21,44 +14,24 @@ They get created either from the recipe sign up flow, through the migration API ## Prerequisites This section assumes that you are familiar with basic **SuperTokens** concepts like **recipes**, **sessions** and authentication methods. -If not, please refer to the [quickstart guide](/docs/quickstart/introduction) first. +If not, please refer to the [quickstart guide](/quickstart) first. ## Customization Besides the common actions which you can use to manage users, you can also integrate different features which allow you extend the user functionality. - - - - Common actions - - - See how to create, update and delete users. - - - - - User Metadata - - - Learn how to include additional data to each user. - - - - - Allow users to update their data - - - Implement a series of steps that enable users to change their credentials. - - - - - Account deduplication - - - Prevent the creation of multiple accounts with the same email ID. - - - + + +See how to create, update and delete users. + + +Learn how to include additional data to each user. + + +Implement a series of steps that enable users to change their credentials. + + +Prevent the creation of multiple accounts with the same email ID. + + diff --git a/docs/post-authentication/user-management/meta.ts b/docs/post-authentication/user-management/meta.ts new file mode 100644 index 0000000000..214f90e7c2 --- /dev/null +++ b/docs/post-authentication/user-management/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "User Management", + icon: "user-search", + order: 2, +}); diff --git a/docs/post-authentication/user-management/progressive-profiling.mdx b/docs/post-authentication/user-management/progressive-profiling.mdx index d1c0f25fb0..9afd39ddbf 100644 --- a/docs/post-authentication/user-management/progressive-profiling.mdx +++ b/docs/post-authentication/user-management/progressive-profiling.mdx @@ -1,13 +1,10 @@ --- title: Progressive profiling -hide_title: true -sidebar_position: 6 description: Gradually collect user information through customizable multi-step forms using the progressive profiling plugin -page_type: tutorial +sidebar: + order: 80 --- -# Progressive profiling - ## Overview This tutorial shows you how to add progressive profiling functionality to your **SuperTokens** authentication flows. @@ -26,10 +23,10 @@ The progressive profiling plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. The implementation is in early stages and APIs might change. -For more information on how plugins work refer to the [references page](/docs/references/plugins/introduction). +For more information on how plugins work refer to the [references page](/references/plugins/introduction). You need to start from a working **SuperTokens** setup. -If you haven't done that already, please refer to the [Quickstart Guides](/docs/quickstart/introduction). +If you haven't done that already, please refer to the [Quickstart Guides](/quickstart). ## Steps @@ -49,7 +46,8 @@ import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profili SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes (Session recipe is required) @@ -139,7 +137,7 @@ The plugin supports the following field types: | `url` | URL input with validation | Website, social profiles | | `image-url` | Image URL input with preview | Profile picture, logo | -:::info no-title +:::info The plugin provides built-in validation for form fields: - **Required Fields**: Automatically validates that required fields are not empty - **Field Type Validation**: Ensures values match the expected field type @@ -161,7 +159,9 @@ import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profili SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ // your recipes @@ -191,7 +191,7 @@ Try to authenticate and check if you get sent to the new form. Progressive profiling setup interface @@ -203,7 +203,7 @@ By default, the plugin stores profile data using the `User Metadata` recipe. You can also implement your custom storage solution by overriding the `defaultStorageHandlerSetFields` and `defaultStorageHandlerGetFields` functions. -```typescript +```typescript check=false reason="Partial configuration example" import SuperTokens from "supertokens-node"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-nodejs"; @@ -250,37 +250,38 @@ SuperTokens.init({ ], }, ], - override: (oI) => ({ - ...oI, - defaultStorageHandlerSetFields: ({ pluginFormFields, data, session, userContext }) => { - const userId = session.getUserId(userContext); - const profile = pluginFormFields.reduce( - (acc, field) => { - const newValue = data.find((d) => d.fieldId === field.id)?.value; - const existingValue = existingProfile?.[field.id]; - return { - ...acc, - [field.id]: newValue ?? existingValue ?? field.defaultValue, - }; - }, - { ...existingProfile } - ); - - // Implement your own logic for storing profile data - await customSetProfileData(userId, profile); - }, - defaultStorageHandlerGetFields: ({ pluginFormFields, session, userContext }) => { - const userId = session.getUserId(userContext); - // Implement your own logic for fetching profile data - const existingProfile = await customGetProfileData(userId); - - return pluginFormFields.map((field) => ({ - sectionId: field.sectionId, - fieldId: field.id, - value: existingProfile[field.id] ?? field.defaultValue, - }); - }, - }), + override: (oI) => ({ + ...oI, + defaultStorageHandlerSetFields: async ({ pluginFormFields, data, session, userContext }) => { + const userId = session.getUserId(userContext); + const existingProfile = await customGetProfileData(userId); + const profile = pluginFormFields.reduce( + (acc, field) => { + const newValue = data.find((d) => d.fieldId === field.id)?.value; + const existingValue = existingProfile?.[field.id]; + return { + ...acc, + [field.id]: newValue ?? existingValue ?? field.defaultValue, + }; + }, + { ...existingProfile }, + ); + + // Implement your own logic for storing profile data + await customSetProfileData(userId, profile); + }, + defaultStorageHandlerGetFields: ({ pluginFormFields, session, userContext }) => { + const userId = session.getUserId(userContext); + // Implement your own logic for fetching profile data + const existingProfile = await customGetProfileData(userId); + + return pluginFormFields.map((field) => ({ + sectionId: field.sectionId, + fieldId: field.id, + value: existingProfile[field.id] ?? field.defaultValue, + })); + }, + }), }), ], }, @@ -290,9 +291,9 @@ SuperTokens.init({ ### User interface To create your own UI you can use the `usePluginContext` hook. -It exposes an interface which you can use to interface with the endpoints exposed by the backend plugin. +It exposes an interface which you can use to interface with the endpoints exposed by the backend plugin. -```typescript +```tsx check=false reason="Requires surrounding application context" import { usePluginContext } from "@supertokens-plugins/progressive-profiling-react"; function CustomProfileComponent() { @@ -325,15 +326,15 @@ function CustomProfileComponent() { } ``` -:::info pre-built UI -You can integrate the pre-built UI into other pages/components by importing the `ProgressiveProfilingWrapper` component: +:::info[pre-built UI] +You can integrate the pre-built UI into other pages/components by importing the `UserProfileWrapper` component: -```typescript -import { ProgressiveProfilingWrapper } from “@supertokens-plugins/progressive-profiling-react”; +```tsx +import { UserProfileWrapper } from "@supertokens-plugins/progressive-profiling-react"; function MyApp() { return (
- +
); } @@ -341,12 +342,11 @@ function MyApp() { ::: - ### Form fields validation To change the default validation behavior you can override the `validateField` function: -```ts +```ts check=false reason="Partial configuration example" import SuperTokens from "supertokens-node"; import ProgressiveProfilingPlugin from "@supertokens-plugins/progressive-profiling-nodejs"; @@ -393,54 +393,38 @@ SuperTokens.init({ ], }, ], - override: (oI) => ({ - ...oI, - validateField: ({ field, value }) => { - // Custom logic to validate field values - if(field.required && value === "") { - return "Field is required"; - } - - // Return undefined if validation passes - return undefined; - }, - }), + override: (oI) => ({ + ...oI, + validateField: ({ field, value }) => { + // Custom logic to validate field values + if (field.required && value === "") { + return "Field is required"; + } + + // Return undefined if validation passes + return undefined; + }, + }), }), ], }, }); - ``` ## Next steps Besides progressive profiling, you can also explore other user management features: - - - - User Banning - - - Implement user banning functionality to restrict access. - - - - - - User Roles - - - Implement role-based access control for your users. - - - - - - Plugins Reference - - - General information on how plugins work. - - - + + +Implement user banning functionality to restrict access. + + + +Implement role-based access control for your users. + + + +General information on how plugins work. + + diff --git a/docs/post-authentication/user-management/user-banning.mdx b/docs/post-authentication/user-management/user-banning.mdx index 75938a0f20..77cf2650da 100644 --- a/docs/post-authentication/user-management/user-banning.mdx +++ b/docs/post-authentication/user-management/user-banning.mdx @@ -1,13 +1,10 @@ --- title: User banning -hide_title: true -sidebar_position: 5 description: Implement a user banning feature using plugins -page_type: tutorial +sidebar: + order: 70 --- -# User banning - ## Overview This tutorial shows you how to add a user banning feature to your SuperTokens authentication flows. @@ -27,7 +24,7 @@ If you are working with a serverless environment or with distributed application ## Before you start -The user banning plugin supports only the `React` and `NodeJS` SDKs. +The user banning plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. Besides initializing the plugin, you also have to include the `UserRoles` recipe in your SuperTokens configuration. @@ -51,7 +48,8 @@ import UserRoles from "supertokens-node/recipe/userroles"; SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ UserRoles.init(), // Required: UserRoles recipe must be initialized @@ -60,16 +58,16 @@ SuperTokens.init({ experimental: { plugins: [ UserBanningPlugin.init({ - userBanningPermission: "ban-user", // Optional: defaults to "ban-user" - bannedUserRole: "banned", // Optional: defaults to "banned" + userBanningPermission: "ban-user", // Optional: defaults to "ban-user" + bannedUserRole: "banned", // Optional: defaults to "banned" }), ], - } + }, }); ``` -:::warning no-title +:::warning Make sure to also initialize the `UserRoles` recipe if you haven't already. ::: @@ -89,7 +87,9 @@ import UserBanningPlugin from "@supertokens-plugins/user-banning-react"; SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ // your recipes @@ -97,12 +97,12 @@ SuperTokens.init({ experimental: { plugins: [ UserBanningPlugin.init({ - userBanningPermission: "ban-user", // Should match backend config - bannedUserRole: "banned", // Should match backend config - onPermissionFailureRedirectPath: "/", // Optional: defaults to "/" + userBanningPermission: "ban-user", // Should match backend config + bannedUserRole: "banned", // Should match backend config + onPermissionFailureRedirectPath: "/", // Optional: defaults to "/" }), ], - } + }, }); ``` @@ -110,15 +110,15 @@ SuperTokens.init({ #### 3.1 Using the user banning interface -The plugin provides a complete administrative interface accessible at `/admin/ban-user`. +The plugin provides a complete administrative interface accessible at `/admin/ban-user`. Before you access the interface, make sure that your user has the required permission, `ban-user` by default. -Read the [role management actions page](/docs/additional-verification/user-roles/role-management-actions#add-permissions) for instructions on how to add permissions to your users. +Read the [role management actions page](/additional-verification/user-roles/role-management-actions#add-permissions) for instructions on how to add permissions to your users. User banning UI @@ -131,7 +131,7 @@ You can also manage user bans programmatically using the exposed API endpoints. ##### Ban/unban user -```javascript +```javascript check=false reason="Requires surrounding async application context" // Ban a user const banResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?tenantId=public", { method: "POST", @@ -142,7 +142,7 @@ const banResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?ten body: JSON.stringify({ email: "user@example.com", // You can also pass the userId instead of the email - // userId: "user123", + // userId: "user123", isBanned: true, // true to ban, false to remove ban }), }); @@ -170,14 +170,14 @@ const unbanResponse = await fetch("/plugin/supertokens-plugin-user-banning/ban?t ##### Check ban status -```javascript +```javascript check=false reason="Requires surrounding async application context" // Check if a user is banned const statusResponse = await fetch( "/plugin/supertokens-plugin-user-banning/ban?tenantId=public&email=user@example.com", { method: "GET", credentials: "include", - } + }, ); const status = await statusResponse.json(); @@ -189,7 +189,6 @@ if (status.status === "OK") { ``` - ## Customization ### Implement a custom user interface @@ -197,7 +196,7 @@ if (status.status === "OK") { To create a custom user interface you can use the `usePluginContext` hook. It allows you to access the plugin's API methods and configuration in custom React components: -```typescript +```tsx import { usePluginContext } from "@supertokens-plugins/user-banning-react"; function MyCustomAdminComponent() { @@ -232,12 +231,8 @@ function MyCustomAdminComponent() { return (

{t("PL_UB_BAN_PAGE_TITLE")}

- - + +
); } @@ -247,31 +242,16 @@ function MyCustomAdminComponent() { Besides user banning you can also look into other user management features and security measures: - - - - Attack Protection Suite - - - Prevent suspicious authentication attempts. - - - - - - User Roles - - - Implement role-based access control for your users. - - - - - - Plugins Reference - - - General information on how plugins work. - - - + + +Prevent suspicious authentication attempts. + + + +Implement role-based access control for your users. + + + +General information on how plugins work. + + diff --git a/docs/post-authentication/user-management/user-metadata.mdx b/docs/post-authentication/user-management/user-metadata.mdx index 77d2366362..9039c0d93a 100644 --- a/docs/post-authentication/user-management/user-metadata.mdx +++ b/docs/post-authentication/user-management/user-metadata.mdx @@ -1,16 +1,10 @@ --- title: User metadata -hide_title: true -sidebar_position: 4 description: Store and manage user metadata using the backend SDK's UserMetadata recipe. -page_type: guide -recipe: emailpassword -category: authentication +sidebar: + order: 50 --- - -# User metadata - ## Overview You can use the `UserMetadata` recipe to store your custom data about each user. @@ -21,33 +15,29 @@ The following page shows you how to enable and use the feature. ## Enable the `UserMetadata` recipe - - - + + ```tsx import SuperTokens from "supertokens-node"; import UserMetadata from "supertokens-node/recipe/usermetadata"; SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - // Initialize other recipes as seen in the quick setup guide - // highlight-next-line - UserMetadata.init(), - ] + supertokens: { + connectionURI: "...", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + // Initialize other recipes as seen in the quick setup guide + UserMetadata.init(), + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/usermetadata" @@ -58,17 +48,14 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ // Initialize other recipes as seen in the quick setup guide - // highlight-next-line usermetadata.Init(nil), }, }) } ``` - - - - -```python + + +```python check=false reason="Partial configuration example" from supertokens_python import InputAppInfo, init from supertokens_python.recipe import usermetadata @@ -76,31 +63,37 @@ init( app_info=InputAppInfo( api_domain="...", app_name="...", website_domain="..." ), - framework='...', # type: ignore + framework='...', recipe_list=[ # Initialize other recipes as seen in the quick setup guide - # highlight-next-line usermetadata.init() ] ) ``` - - - + + --- ## Store data -:::important -Only root-level properties merge into the stored object. Nested objects and all lower-level properties replace the existing ones. +:::note[Only root-level properties merge into the stored object. Nested objects and all lower-level properties replace the existing ones.] ::: - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -112,17 +105,13 @@ app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end res.json({ message: "successfully updated user metadata" }); }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -145,17 +134,13 @@ server.route({ const session = req.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` - - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -172,17 +157,13 @@ fastify.post( const session = req.session; const userId = session.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end res.send({ message: "successfully updated user metadata" }); }, ); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; @@ -192,9 +173,7 @@ async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end return { body: JSON.stringify({ message: "successfully updated user metadata" }), @@ -204,10 +183,8 @@ async function updateinfo(awsEvent: SessionEvent) { exports.handler = verifySession(updateinfo); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -220,16 +197,12 @@ router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => const session = ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end ctx.body = { message: "successfully updated user metadata" }; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; @@ -246,21 +219,13 @@ class UpdateInfo { const session = this.ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end return { message: "successfully updated user metadata" }; } } ``` - - - - - - - - + + ```tsx @@ -280,51 +245,18 @@ export default async function updateInfo(req: any, res: any) { const session = (req as SessionRequest).session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end res.json({ message: "successfully updated user metadata" }); } ``` - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const userId = session!.getUserId(); - - // highlight-start - await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - // highlight-end - return NextResponse.json({ success: "successfully updated user metadata" }); - }); -} -``` - - - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore import { AuthGuard } from "./auth/auth.guard"; @Controller() @@ -335,39 +267,30 @@ export class ExampleController { async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); - //highlight-start await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); - //highlight-end return { message: "successfully updated user metadata" }; } } ``` - - - - - - + + + + ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." - // highlight-start usermetadata.UpdateUserMetadata(userId, map[string]interface{}{ "newKey": "data", }) - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata @@ -375,35 +298,62 @@ from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata async def some_func(): user_id = "..." - # highlight-start await update_user_metadata(user_id, { "newKey": "data" }) - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.usermetadata.syncio import update_user_metadata user_id = "..." -# highlight-start update_user_metadata(user_id, { "newKey": "data" }) -# highlight-end +``` + + + + + + + + + + + +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const userId = session!.getUserId(); + + await UserMetadata.updateUserMetadata(userId, { newKey: "data" }); + return NextResponse.json({ success: "successfully updated user metadata" }); + }); +} ``` - - - - + + + + + -:::info Multi Tenancy +:::info[Multi Tenancy] User metadata that associates with a user shares across all tenants that that user is a part of. If instead, you want to store user metadata on a tenant level, you can add a custom key in the JSON like: ```json @@ -425,11 +375,20 @@ and then read the appropriate key based on the `tenantId`. ## Access data - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -441,17 +400,13 @@ app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end res.json({ preferences: metadata.preferences }); }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -474,17 +429,13 @@ server.route({ const session = req.session; const userId = session!.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end return res.response({ preferences: metadata.preferences }).code(200); }, }); ``` - - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -501,17 +452,13 @@ fastify.post( const session = req.session; const userId = session.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end res.send({ preferences: metadata.preferences }); }, ); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import UserMetadata from "supertokens-node/recipe/usermetadata"; @@ -521,9 +468,7 @@ async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end return { body: JSON.stringify({ preferences: metadata.preferences }), @@ -533,10 +478,8 @@ async function updateinfo(awsEvent: SessionEvent) { exports.handler = verifySession(updateinfo); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -549,16 +492,12 @@ router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => const session = ctx.session; const userId = session!.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end ctx.body = { preferences: metadata.preferences }; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; @@ -575,21 +514,13 @@ class UpdateInfo { const session = this.ctx.session; const userId = session!.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end return { preferences: metadata.preferences }; } } ``` - - - - - - - - + + ```tsx @@ -609,51 +540,18 @@ export default async function updateInfo(req: any, res: any) { const session = (req as SessionRequest).session; const userId = session!.getUserId(); - // highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end res.json({ preferences: metadata.preferences }); } ``` - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const userId = session!.getUserId(); - - // highlight-start - const { metadata } = await UserMetadata.getUserMetadata(userId); - // highlight-end - return NextResponse.json({ preferences: metadata.preferences }); - }); -} -``` - - - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore import { AuthGuard } from "./auth/auth.guard"; @Controller() @@ -664,19 +562,15 @@ export class ExampleController { async postExample(@Session() session: SessionContainer): Promise<{ preferences: any }> { const userId = session.getUserId(); - //highlight-start const { metadata } = await UserMetadata.getUserMetadata(userId); - //highlight-end return { preferences: metadata.preferences }; } } ``` - - - - - - + + + + ```go import ( "fmt" @@ -687,7 +581,6 @@ import ( func main() { userId := "..." - // highlight-next-line metadata, err := usermetadata.GetUserMetadata(userId) if err != nil { // TODO: handle error... @@ -697,13 +590,10 @@ func main() { fmt.Println(exampleValue) } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata @@ -711,32 +601,61 @@ from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata async def some_func(): user_id = "..." - # highlight-next-line metadataResult = await get_user_metadata(user_id) exampleValue = metadataResult.metadata["exampleKey"] print(exampleValue) ``` - - - - + + ```python from supertokens_python.recipe.usermetadata.syncio import get_user_metadata user_id = "..." -# highlight-next-line metadataResult = get_user_metadata(user_id) exampleValue = metadataResult.metadata["exampleKey"] print(exampleValue) ``` + + + + + + + + + + - - - - +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { backendConfig } from "@/app/config/backend"; -:::info Important +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const userId = session!.getUserId(); + + const { metadata } = await UserMetadata.getUserMetadata(userId); + return NextResponse.json({ preferences: metadata.preferences }); + }); +} +``` + + + + + + + +:::info[Important] By default, all users have an empty metadata object. ::: @@ -745,7 +664,7 @@ By default, all users have an empty metadata object. You can either delete all the user's metadata, or certain fields from them: -### Delete specific fields +### Delete specific fields You can do this by calling the update metadata function and setting the field you want to remove to be `null`. For example, if you have the following metadata object for a user: ```json @@ -771,17 +690,26 @@ This would result in the final metadata object: } ``` -:::info Important +:::info[Important] You can only remove the root level fields in the metadata object in this way. From the above example, if you set `preferences.theme: null`, then it does not remove the `"theme"` field, but instead sets it to a JSON null value. ::: In code, it would look like: - - - - - + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -793,17 +721,13 @@ app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end res.json({ message: "successfully updated user metadata" }); }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -826,17 +750,13 @@ server.route({ const session = req.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end return res.response({ message: "successfully updated user metadata" }).code(200); }, }); ``` - - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -853,17 +773,13 @@ fastify.post( const session = req.session; const userId = session.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end res.send({ message: "successfully updated user metadata" }); }, ); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; @@ -873,9 +789,7 @@ async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end return { body: JSON.stringify({ message: "successfully updated user metadata" }), @@ -885,10 +799,8 @@ async function updateinfo(awsEvent: SessionEvent) { exports.handler = verifySession(updateinfo); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -901,16 +813,12 @@ router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => const session = ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end ctx.body = { message: "successfully updated user metadata" }; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; @@ -927,21 +835,13 @@ class UpdateInfo { const session = this.ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end return { message: "successfully updated user metadata" }; } } ``` - - - - - - - - + + ```tsx @@ -961,50 +861,18 @@ export default async function updateInfo(req: any, res: any) { const session = (req as SessionRequest).session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end res.json({ message: "successfully updated user metadata" }); } ``` - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const userId = session!.getUserId(); - - // highlight-start - await UserMetadata.updateUserMetadata(userId, { notifications: null }); - // highlight-end - return NextResponse.json({ message: "successfully updated user metadata" }); - }); -} -``` - - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore import { AuthGuard } from "./auth/auth.guard"; @Controller() @@ -1015,39 +883,30 @@ export class ExampleController { async postExample(@Session() session: SessionContainer): Promise<{ message: string }> { const userId = session.getUserId(); - //highlight-start await UserMetadata.updateUserMetadata(userId, { notifications: null }); - //highlight-end return { message: "successfully updated user metadata" }; } } ``` - - - - - - + + + + ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." - // highlight-start usermetadata.UpdateUserMetadata(userId, map[string]interface{}{ "notifications": nil, }) - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata @@ -1055,42 +914,78 @@ from supertokens_python.recipe.usermetadata.asyncio import update_user_metadata async def some_func(): user_id = "..." - # highlight-start await update_user_metadata(user_id, { "notifications": None }) - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.usermetadata.syncio import update_user_metadata user_id = "..." -# highlight-start update_user_metadata(user_id, { "notifications": None }) -# highlight-end ``` + + + + + + + + + + - - - - +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { backendConfig } from "@/app/config/backend"; -### Delete the entire metadata object +SuperTokens.init(backendConfig()); -Using this function deletes all the fields in the user metadata object for that user. +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const userId = session!.getUserId(); - - - - + await UserMetadata.updateUserMetadata(userId, { notifications: null }); + return NextResponse.json({ message: "successfully updated user metadata" }); + }); +} +``` + + + + + + +### Delete the entire metadata object + +Using this function deletes all the fields in the user metadata object for that user. + + + + + + + + + + + + + + + ```tsx import express from "express"; import { verifySession } from "supertokens-node/recipe/session/framework/express"; @@ -1102,17 +997,13 @@ app.post("/updateinfo", verifySession(), async (req, res) => { const session = req.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end res.json({ success: true }); }); ``` - - - - + + ```tsx import Hapi from "@hapi/hapi"; import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; @@ -1135,17 +1026,13 @@ server.route({ const session = req.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end return res.response({ success: true }).code(200); }, }); ``` - - - - + + ```tsx import Fastify from "fastify"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; @@ -1162,17 +1049,13 @@ fastify.post( const session = req.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end res.send({ success: true }); }, ); ``` - - - - + + ```tsx import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; import { SessionEvent } from "supertokens-node/framework/awsLambda"; @@ -1182,9 +1065,7 @@ async function updateinfo(awsEvent: SessionEvent) { const session = awsEvent.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end return { body: JSON.stringify({ success: true }), @@ -1194,10 +1075,8 @@ async function updateinfo(awsEvent: SessionEvent) { exports.handler = verifySession(updateinfo); ``` - - - - + + ```tsx import KoaRouter from "koa-router"; import { verifySession } from "supertokens-node/recipe/session/framework/koa"; @@ -1210,16 +1089,12 @@ router.post("/updateinfo", verifySession(), async (ctx: SessionContext, next) => const session = ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end ctx.body = { success: true }; }); ``` - - - - + + ```tsx import { inject, intercept } from "@loopback/core"; import { RestBindings, post, response } from "@loopback/rest"; @@ -1236,21 +1111,13 @@ class UpdateInfo { const session = this.ctx.session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end return { success: true }; } } ``` - - - - - - - - + + ```tsx @@ -1270,50 +1137,18 @@ export default async function updateInfo(req: any, res: any) { const session = (req as SessionRequest).session; const userId = session!.getUserId(); - // highlight-start await UserMetadata.clearUserMetadata(userId); - // highlight-end res.json({ success: true }); } ``` - - -```tsx -import { NextResponse, NextRequest } from "next/server"; -import SuperTokens from "supertokens-node"; -import { withSession } from "supertokens-node/nextjs"; -import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -export function POST(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - const userId = session!.getUserId(); - - // highlight-start - await UserMetadata.clearUserMetadata(userId); - // highlight-end - return NextResponse.json({ success: true }); - }); -} -``` - - - - - -```tsx + + +```tsx check=false reason="Requires surrounding framework application context" import { Controller, Post, UseGuards, Session } from "@nestjs/common"; import { SessionContainer } from "supertokens-node/recipe/session"; import UserMetadata from "supertokens-node/recipe/usermetadata"; -// @ts-ignore import { AuthGuard } from "./auth/auth.guard"; @Controller() @@ -1323,38 +1158,29 @@ export class ExampleController { async postExample(@Session() session: SessionContainer): Promise<{ success: boolean }> { const userId = session.getUserId(); - //highlight-start // For more information about "AuthGuard" and the "Session" decorator please read our NestJS guide. await UserMetadata.clearUserMetadata(userId); - //highlight-end return { success: true }; } } ``` - - - - - - + + + + ```go import "github.com/supertokens/supertokens-golang/recipe/usermetadata" func main() { userId := "..." - // highlight-start usermetadata.ClearUserMetadata(userId) - // highlight-end } ``` - - - - - - - + + + + ```python from supertokens_python.recipe.usermetadata.asyncio import clear_user_metadata @@ -1362,36 +1188,63 @@ from supertokens_python.recipe.usermetadata.asyncio import clear_user_metadata async def some_func(): user_id = "..." - # highlight-start await clear_user_metadata(user_id) - # highlight-end ``` - - - - + + ```python from supertokens_python.recipe.usermetadata.syncio import clear_user_metadata user_id = "..." -# highlight-start clear_user_metadata(user_id) -# highlight-end ``` + + + + + + + + + + - - - - +```tsx check=false reason="Requires surrounding framework application context" +import { NextResponse, NextRequest } from "next/server"; +import SuperTokens from "supertokens-node"; +import { withSession } from "supertokens-node/nextjs"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +export function POST(request: NextRequest) { + return withSession(request, async (err, session) => { + if (err) { + return NextResponse.json(err, { status: 500 }); + } + const userId = session!.getUserId(); + + await UserMetadata.clearUserMetadata(userId); + return NextResponse.json({ success: true }); + }); +} +``` + + + + + + --- ## See also - - - - - - + + + + + + diff --git a/docs/post-authentication/user-management/user-profile.mdx b/docs/post-authentication/user-management/user-profile.mdx index 79a5c863ec..868261d817 100644 --- a/docs/post-authentication/user-management/user-profile.mdx +++ b/docs/post-authentication/user-management/user-profile.mdx @@ -1,19 +1,18 @@ --- title: Profile Management -hide_title: true -sidebar_position: 4 -description: Add comprehensive user profile management with customizable form fields and account information display using the profile details plugin -page_type: tutorial +description: >- + Add comprehensive user profile management with customizable form fields and account information display using the + profile details plugin +sidebar: + order: 40 --- -# User profile management - ## Overview This tutorial shows you how to add comprehensive user profile management to your **SuperTokens** authentication flows. The guide makes use of the `plugins` functionality which provides a complete profile management interface with customizable form fields, account information display, and automatic third-party data integration. -The functionality integrates with the [progressive profiling plugin](/docs/post-authentication/user-management/progressive-profiling) by default. +The functionality integrates with the [progressive profiling plugin](/post-authentication/user-management/progressive-profiling) by default. This allows you to: - Gradually collect user information through the progressive profiling flow - Display collected information in the profile details interface @@ -25,7 +24,7 @@ The profile details plugin supports only the `React` and `NodeJS` SDKs. Support for other platforms is under active development. You need to start from a working **SuperTokens** setup. -If you haven't done that already, please refer to the [Quickstart Guides](/docs/quickstart/introduction). +If you haven't done that already, please refer to the [Quickstart Guides](/quickstart). ## Steps @@ -47,7 +46,8 @@ import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-nodejs"; SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", }, recipeList: [ // your recipes (Session recipe is required) @@ -146,11 +146,11 @@ When users sign in using external providers, the plugin maps provider data to pr - `lastName`: Maps from `family_name` or `last_name` - `avatar`: Maps from `picture` or `avatar_url` -:::info no-title +:::info You can customize how third-party data maps to your profile fields by overriding the function `getFieldValueFromThirdPartyUserInfo`. -```typescript -import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-node"; +```typescript check=false reason="Requires surrounding application context" +import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-nodejs"; SuperTokens.init({ // ... other config @@ -160,7 +160,7 @@ SuperTokens.init({ override: (oI) => ({ ...oI, getFieldValueFromThirdPartyUserInfo: (providerId, field, rawUserInfoFromProvider, profile) => { - return rawUserInfoFromProvider[field.id]; + return rawUserInfoFromProvider[field.id]; }, }), }), @@ -189,21 +189,20 @@ import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ // your recipes ], experimental: { - plugins: [ - ProfileDetailsPlugin.init(), - ], + plugins: [ProfileDetailsPlugin.init()], }, }); ``` -:::info -The user profile page gets rendered by default on the `/user/profile` path. +:::info[The user profile page gets rendered by default on the `/user/profile` path.] If you want to change the path, you have to initialize the `profile-base-react` plugin with the `profilePagePath` option. ```typescript @@ -213,7 +212,9 @@ import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; SuperTokens.init({ appInfo: { - // your app info + appName: "My App", + apiDomain: "https://api.example.com", + websiteDomain: "https://example.com", }, recipeList: [ // your recipes @@ -238,7 +239,7 @@ You should see the new interface that renders the profile data. Progressive profiling setup interface ## Customization @@ -247,7 +248,7 @@ You should see the new interface that renders the profile data. To add custom rendering behavior for fields you have to pass an override during plugin initialization. -```typescript +```typescript check=false reason="Requires surrounding framework application context" import ProfileDetailsPlugin from "@supertokens-plugins/profile-details-react"; import { CustomStringInput, CustomStringView } from "./your-custom-components"; @@ -278,7 +279,7 @@ SuperTokens.init({ To create your own UI you can use the `usePluginContext` hook. It exposes an interface which you can use to call the endpoints exposed by the backend plugin. -```typescript +```tsx check=false reason="Requires surrounding application context" import { usePluginContext } from "@supertokens-plugins/profile-details-react"; function CustomProfileComponent() { @@ -313,40 +314,20 @@ function CustomProfileComponent() { Besides profile details management, you can also explore other user management features: - - - - Progressive Profiling - - - Gradually collect user information through customizable forms. - - - - - - User Banning - - - Implement user banning functionality to restrict access. - - - - - - User Roles - - - Implement role-based access control for your users. - - - - - - Plugins Reference - - - General information on how plugins work. - - - + + +Gradually collect user information through customizable forms. + + + +Implement user banning functionality to restrict access. + + + +Implement role-based access control for your users. + + + +General information on how plugins work. + + diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx new file mode 100644 index 0000000000..f8e28c9dbd --- /dev/null +++ b/docs/quickstart.mdx @@ -0,0 +1,2786 @@ +--- +title: Quickstart Guide +description: Add SuperTokens authentication to your frontend and backend, then prepare the integration for production. +sidebar: + icon: zap + order: 30 +--- + +## Overview + + +Inspect this repository and integrate SuperTokens into the existing application. First discover the frontend stack and backend stack, including languages, frameworks, package managers, routers, SDK versions, existing authentication code, and environment configuration. If the frontend or backend stack cannot be determined reliably, ask the user to provide it before making changes. Also ask which authentication methods and UI approach they need if those choices cannot be inferred. Use the current SuperTokens documentation and SDK APIs, preserve the project's conventions, and do not commit secrets. Configure the frontend, backend, sessions, routes, middleware, cookies, CORS, and environment variables as required. Run the relevant typechecks, tests, and build, then summarize changed files, required environment variables, and validation results. + + +This guide walks through adding Email/Password authentication with either the SuperTokens prebuilt UI or your own custom UI. Configure the frontend first, then connect your backend and prepare the integration for production. + +## Steps + +### 1. Integrate the frontend SDK + + + +#### Frontend integration summary + +- React uses `supertokens-auth-react`; Angular and Vue use `supertokens-web-js`. +- Initialize the authentication and Session recipes. React applications also wrap their component tree with `SuperTokensWrapper`. +- Render the prebuilt login UI on `/auth`. +- The SDK intercepts `fetch` and XHR requests to manage session tokens automatically. Web sessions use HTTP-only cookies by default, with header-based authentication available as an alternative. + + + +Start the setup by configuring your frontend application to use **SuperTokens** for authentication. + +This guide uses the **SuperTokens pre-built UI** components. +If you want to create your own interface please check the **Custom UI** tutorial. + + + + + +1.1 Install the SDK + +Run the following command in your terminal to install the package. + + +```bash title="Reactjs" option="package-managers:npm" + npm i -s supertokens-auth-react +``` + +```bash title="Reactjs" option="package-managers:yarn" +yarn add supertokens-auth-react supertokens-web-js +``` + +```bash title="Reactjs" option="package-managers:pnpm" +pnpm add supertokens-auth-react supertokens-web-js +``` + +```bash title="Reactjs" option="package-managers:bun" +bun add supertokens-auth-react supertokens-web-js +``` + +```bash title="Angular" option="package-managers:npm" +npm i -s supertokens-web-js +``` + +```bash title="Angular" option="package-managers:yarn" +yarn add supertokens-web-js +``` + +```bash title="Angular" option="package-managers:pnpm" +pnpm add supertokens-web-js +``` + +```bash title="Angular" option="package-managers:bun" +bun add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:npm" +npm i -s supertokens-web-js +``` + +```bash title="Vue" option="package-managers:yarn" +yarn add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:pnpm" +pnpm add supertokens-web-js +``` + +```bash title="Vue" option="package-managers:bun" +bun add supertokens-web-js +``` + + +#### 1.2 Initialize the SDK + + + + In your main application file call the `SuperTokens.init` function to initialize the SDK. + The `init` call includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. + After that you have to wrap the application with the `SuperTokensWrapper` component. + This provides authentication context for the rest of the UI tree. + + + Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. + + **Architecture** + + - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. + - You have to create a `/auth*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. + + Creating the `/auth` route + + - Use the Angular CLI to generate a new route + + + Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app + + **Architecture** + + - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. + - We create a `/auth*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. + + **Creating the `/auth` route** + + - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: + + + + +```tsx title="Reactjs" +import React from "react"; + +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; +import Session from "supertokens-auth-react/recipe/session"; + +SuperTokens.init({ + appInfo: { + // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [EmailPassword.init(), Session.init()], +}); + +/* Your App */ +class App extends React.Component { + render() { + return {/*Your app components*/}; + } +} +``` + +```bash title="Angular" + ng generate module auth --route auth --module app.module +``` + +```tsx check=false reason="This is a Vue single-file component containing both TypeScript and template markup." title="Vue" + + + +``` + + + + + + - Add the following code to your `auth` angular component + + + - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. + + - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. + + + + +```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" + import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core"; + import { DOCUMENT } from "@angular/common"; + + @Component({ + selector: "app-auth", + template: '
', + }) + export class AuthComponent implements OnDestroy, AfterViewInit { + constructor( + private renderer: Renderer2, + @Inject(DOCUMENT) private document: Document, + ) {} + + ngAfterViewInit() { + this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@v0.48.0/build/static/js/main.81589a39.js"); + } + + ngOnDestroy() { + // Remove the script when the component is destroyed + const script = this.document.getElementById("supertokens-script"); + if (script) { + script.remove(); + } + } + + private loadScript(src: string) { + const script = this.renderer.createElement("script"); + script.type = "text/javascript"; + script.src = src; + script.id = "supertokens-script"; + script.onload = () => { + supertokensUIInit("supertokensui", { + appInfo: { + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [supertokensUIEmailPassword.init(), supertokensUISession.init()], + }); + }; + this.renderer.appendChild(this.document.body, script); + } + } +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" + import { createApp } from "vue"; + import SuperTokens from "supertokens-web-js"; + import Session from "supertokens-web-js/recipe/session"; + import App from "./App.vue"; + import router from "./router"; + + SuperTokens.init({ + appInfo: { + appName: "", + apiDomain: "", + apiBasePath: "/auth", + }, + recipeList: [Session.init()], + }); + + const app = createApp(App); + + app.use(router); + + app.mount("#app"); +``` +
+ + + + + - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the `emailpassword` and session recipes. + + - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. + + + + + +```tsx title="Angular" +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; + +SuperTokens.init({ + appInfo: { + appName: "", + apiDomain: "", + apiBasePath: "/auth", + }, + recipeList: [Session.init()], +}); +``` + + +#### 1.3 Configure routing + + + + In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. + The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. + Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. + + + + Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. + + + Add the route handling shown below to your root-level `render` function. + + + + + Update your angular router so that all auth related requests load the `auth` component + + + Update your Vue router so that all auth related requests load the `AuthView` component + + + + +```tsx title="Reactjs" option="react-router:yes" +import React from "react"; +import { BrowserRouter, Routes, Route, Link } from "react-router-dom"; + +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import * as reactRouterDom from "react-router-dom"; + +class App extends React.Component { + render() { + return ( + + + + {/*This renders the login UI on the /auth route*/} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} + {/*Your app routes*/} + + + + ); + } +} +``` + +```tsx title="Reactjs" option="react-router:no" +import React from "react"; +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; + +class App extends React.Component { + render() { + if (canHandleRoute([EmailPasswordPreBuiltUI])) { + // This renders the login UI on the /auth route + return getRoutingComponent([EmailPasswordPreBuiltUI]); + } + + return {/*Your app*/}; + } +} +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Angular" + import { NgModule } from "@angular/core"; + import { RouterModule, Routes } from "@angular/router"; + + const routes: Routes = [ + { + path: "auth", + loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), + }, + + { + path: "**", + loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), + }, + ]; + + @NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + }) + export class AppRoutingModule {} +``` + +```tsx check=false reason="Requires surrounding quickstart application context" title="Vue" + import { createRouter, createWebHistory } from "vue-router"; + import HomeView from "../views/HomeView.vue"; + import AuthView from "../views/AuthView.vue"; + + const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + name: "home", + component: HomeView, + }, + { + path: "/auth/:pathMatch(.*)*", + name: "auth", + component: AuthView, + }, + ], + }); + + export default router; +``` + + + + + + + :::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.] + Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details. + ::: + + + + + + + + + +```tsx title="Reactjs" option="react-router:yes" +import React from "react"; + +import { BrowserRouter, useRoutes } from "react-router-dom"; +import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; +import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; +import * as reactRouterDom from "react-router-dom"; + +function AppRoutes() { + const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */ + ]); + + const routes = useRoutes([ + ...authRoutes.map((route) => route.props), + // Include the rest of your app routes + ]); + + return routes; +} + +function App() { + return ( + + + + + + ); +} +``` + + +#### 1.4 Handle session tokens + +This part is handled automatically by the **Frontend SDK**. +You don't need to do anything. +The step serves more as a way for us to tell you how is this handled under the hood. + +After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. +The interceptors save the session tokens that are generated from the authentication flow. +Those tokens are then added to requests initialized by your frontend app which target the backend API. +By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/post-authentication/session-management/switch-between-cookies-and-header-authentication). + +#### 1.5 Secure application routes + +In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. +Follow the code samples below to understand how to do this. + + + + You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. + + + You can use the `doesSessionExist` function to check if a session exists in all your routes. + + + You can use the `doesSessionExist` function to check if a session exists in all your routes. + + + + +```tsx check=false reason="Requires surrounding quickstart application context" title="Reactjs" +import React from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import MyDashboardComponent from "./dashboard"; + +class App extends React.Component { + render() { + return ( + + + + {/*Components that require to be protected by authentication*/} + +
+ } + /> + + + ); + } +} +``` + +```tsx title="Angular" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```tsx title="Vue" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + + + + + + +1.1 Install the SDK + +Use the following command to install the required package. + + + + + + + + + :::info + If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/authentication/unified-login/introduction). + ::: + + + + Add to your `settings.gradle`: + + Using CocoaPods + + Add the CocoaPods dependency to your `Podfile` + + Add the dependency to your pubspec.yaml + + + + + +```bash title="Web" option="install-method:npm" +npm i -s supertokens-web-js +``` + +```bash title="Mobile" option="mobile-frameworks:reactnative" +npm i -s supertokens-react-native@5.1.5 @react-native-async-storage/async-storage@2.2.0 +``` + +```bash title="Mobile" option="mobile-frameworks:android" +dependencyResolutionManagement { + ... + repositories { + ... + maven { url 'https://jitpack.io' } + } +} +``` + +```bash title="Mobile" option="mobile-frameworks:ios" +pod 'SuperTokensIOS', '0.4.2' +``` + +```bash title="Mobile" option="mobile-frameworks:flutter" +supertokens_flutter: 0.6.5 +``` + + + + + + + + + Add the following to you app level's `build.gradle`: + + + ##### Using Swift Package Manager + + Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. + + When adding the dependency, select version `0.4.2` after you enter the SuperTokens iOS repository URL: + + + You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). + + + + + + +```bash title="Mobile" option="mobile-frameworks:android" +implementation 'com.github.supertokens:supertokens-android:0.5.3' +``` + +```bash title="Mobile" option="mobile-frameworks:ios" +https://github.com/supertokens/supertokens-ios +``` + + + + + + + + + You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). + + + + + + + +#### 1.2 Initialize SuperTokens + + +Call the SDK init function at the start of your application. +The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. + + + + + + + + Add the `SuperTokens.init` function call at the start of your application. + + + + + + + + +```tsx title="Web" option="install-method:npm" +import SuperTokens from "supertokens-web-js"; +import Session from "supertokens-web-js/recipe/session"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; + +SuperTokens.init({ + appInfo: { + apiDomain: "", + apiBasePath: "/auth", + appName: "...", + }, + recipeList: [Session.init(), EmailPassword.init()], +}); +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +SuperTokens.init({ + apiDomain: "", + apiBasePath: "/auth", +}); +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + override fun onCreate() { + super.onCreate() + + SuperTokens.Builder(this, "") + .apiBasePath("/auth") + .build() + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + do { + try SuperTokens.initialize( + apiDomain: "", + apiBasePath: "/auth" + ) + } catch SuperTokensError.initError(let message) { + // TODO: Handle initialization error + } catch { + // Some other error + } + + return true + } + +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +void main() { + SuperTokens.init( + apiDomain: "", + apiBasePath: "/auth", + ); +} +``` + + + +#### 1.3 Add the login UI + +The **Email/Password** flow involves two types of user interfaces. +One for registering and creating new users, the *Sign Up Form*. +And one for the actual authentication attempt, the *Sign In Form*. +If you are provisioning users from a different method you can skip over adding the sign up form. + +##### 1.3.1 Add the sign-up form + + + + For the **Sign Up** flow you have to first add the UI elements which render your form. + After that, call the following function when the user submits the form that you have previously created. + + + For the **Sign Up** flow you have to first add the UI elements which render your form. + After that, call the following API when the user submits the form that you have previously created. + + + + +```tsx title="Web" option="install-method:npm" +import { signUp } from "supertokens-web-js/recipe/emailpassword"; + +async function signUpClicked(email: string, password: string) { + try { + let response = await signUp({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + // one of the input formFields failed validation + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax), + // or the email is not unique. + window.alert(formField.error); + } else if (formField.id === "password") { + // Password validation failed. + // Maybe it didn't match the password strength + window.alert(formField.error); + } + }); + } else if (response.status === "SIGN_UP_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign up was not allowed. + window.alert(response.reason); + } else { + // sign up successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/homepage"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request POST '/auth/signup' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "formFields": [{ + "id": "email", + "value": "john@example.com" + }, { + "id": "password", + "value": "somePassword123" + }] +}' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. + - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: + - The email could fail validation if it's syntactically not an email, of it it's not unique. + - The password could fail validation if it's not string enough (as defined by the backend password validator). + + Either way, you want to show the user an error next to the input form field. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. + + + +The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: + +```json +{ + "formFields": [ + { + "id": "email", + "value": "john@example.com" + }, + { + "id": "password", + "value": "somePassword123" + }, + { + "id": "name", + "value": "John Doe" + } + ] +} +``` + +On the backend, the `formFields` array is available to you for consumption. + +On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. + +###### How to check if an email is unique + +As a part of the sign up form, you may want to explicitly check that the entered email is unique. +Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. + + + +```tsx title="Web" option="install-method:npm" +import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; + +async function checkEmail(email: string) { + try { + let response = await doesEmailExist({ + email, + }); + + if (response.doesExist) { + window.alert("Email already exists. Please sign in instead"); + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request GET '/auth/emailpassword/email/exists?email=john@example.com' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + + + +##### 1.3.2 Add the sign-in form + + + + + For the **Sign In** flow you have to first add the UI elements which render your form. + After that, call the following function when the user submits the form that you have previously created. + + + For the **Sign In** flow you have to first add the UI elements which render your form. + After that, call the following API when the user submits the form that you have previously created. + + + + +```tsx title="Web" option="install-method:npm" +import { signIn } from "supertokens-web-js/recipe/emailpassword"; + +async function signInClicked(email: string, password: string) { + try { + let response = await signIn({ + formFields: [ + { + id: "email", + value: email, + }, + { + id: "password", + value: password, + }, + ], + }); + + if (response.status === "FIELD_ERROR") { + response.formFields.forEach((formField) => { + if (formField.id === "email") { + // Email validation failed (for example incorrect email syntax). + window.alert(formField.error); + } + }); + } else if (response.status === "WRONG_CREDENTIALS_ERROR") { + window.alert("Email password combination is incorrect."); + } else if (response.status === "SIGN_IN_NOT_ALLOWED") { + // the reason string is a user friendly message + // about what went wrong. It can also contain a support code which users + // can tell you so you know why their sign in was not allowed. + window.alert(response.reason); + } else { + // sign in successful. The session tokens are automatically handled by + // the frontend SDK. + window.location.href = "/homepage"; + } + } catch (err: any) { + if (err.isSuperTokensGeneralError === true) { + // this may be a custom error message sent from the API by you. + window.alert(err.message); + } else { + window.alert("Oops! Something went wrong."); + } + } +} +``` + +```bash title="Mobile" +curl --location --request POST '/auth/signin' \ +--header 'Content-Type: application/json; charset=utf-8' \ +--data-raw '{ + "formFields": [{ + "id": "email", + "value": "john@example.com" + }, { + "id": "password", + "value": "somePassword123" + }] +}' +``` + + + + + + The response body from the API call has a `status` property in it: + - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. + - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. + - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. + - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. + - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. + + + +On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. + +#### 1.4 Handle session tokens + +You can use sessions with SuperTokens in two modes: +- Using `httpOnly` cookies +- Authorization bearer token. + +Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. +For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. + +##### With the Frontend SDK + + + + :::success[No action required.] + ::: + + Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. + + Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. + + By default, our web SDKs use cookies to provide credentials. + + + + + Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + ###### Axios + + ###### Using a custom Axios instance + + + ###### HttpURLConnection + + + ###### `URLSession` + + ###### Using `URLSession.shared` + + + ###### `http` + + You can make requests as you normally would with `http`, the only difference is that you import the client from the SuperTokens package instead. + + + + + + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import axios from "axios"; +import SuperTokens from "supertokens-react-native"; + +let axiosInstance = axios.create({ + /*...*/ +}); +SuperTokens.addAxiosInterceptors(axiosInstance); + +async function callAPI() { + // use axios as you normally do + let response = await axiosInstance.get("https://yourapi.com"); +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens +import com.supertokens.session.SuperTokensHttpURLConnection +import com.supertokens.session.SuperTokensPersistentCookieStore +import java.net.URL +import java.net.HttpURLConnection + +class MainApplication: Application() { + override fun onCreate() { + super.onCreate() + // TODO: Make sure to call SuperTokens.init + } + + fun makeRequest() { + val url = URL("") + val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { + override fun doAction(con: HttpURLConnection?) { + // TODO: Use `con` to set request method, headers etc + } + }) + + // Handle response using connection object, for example: + if (connection.responseCode == 200) { + // TODO: implement + } + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + URLProtocol.registerClass(SuperTokensURLProtocol.self) + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:http/http.dart' as base_http; +import 'package:supertokens_flutter/http.dart' as supertokens_http; + +Future makeRequest() async { + Uri uri = Uri.parse("http://localhost:3001/api"); + var response = await http.get(uri); + // handle response +} +``` + + + + + + + + ###### Using the global Axios instance + + :::note[You must call `addAxiosInterceptors` on all `axios` imports.] + ::: + + + :::note[When making network requests you do not need to call `HttpURLConnection.connect` because SuperTokens does this for you.] + ::: + + ###### OkHttp or Retrofit + + + ###### Using a custom `URLSession` instance + + + ###### Using a custom HTTP client + + If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. + + + + + + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import axios from "axios"; +import SuperTokens from "supertokens-react-native"; +SuperTokens.addAxiosInterceptors(axios); + +async function callAPI() { + // use axios as you normally do + let response = await axios.get("https://yourapi.com"); +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.content.Context +import com.supertokens.session.SuperTokens +import com.supertokens.session.SuperTokensInterceptor +import okhttp3.OkHttpClient +import retrofit2.Retrofit + +class NetworkManager { + fun getClient(context: Context): OkHttpClient { + val clientBuilder = OkHttpClient.Builder() + clientBuilder.addInterceptor(SuperTokensInterceptor()) + // TODO: Make sure to call SuperTokens.init + + val client = clientBuilder.build() + + // REQUIRED FOR RETROFIT ONLY + val instance = Retrofit.Builder() + .baseUrl("") + .client(client) + .build() + + return client + } + + fun makeRequest(context: Context) { + val client = getClient(context) + // Use client to make requests normally + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [SuperTokensURLProtocol.self] + let session = URLSession(configuration: configuration) + + // Use session when making network requests + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +// Import http from the SuperTokens package +import 'package:supertokens_flutter/http.dart' as http; + +Future makeRequest() async { + Uri uri = Uri.parse("http://localhost:3001/api"); + + var customClient = base_http.Client(); + var httpClient = supertokens_http.Client(client: customClient); + + var response = await httpClient.get(uri); + // handle response +} +``` + + + + + + + + ###### Fetch + + :::success[When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here.] + ::: + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + ###### Alamofire + + + ###### Dio + + ###### Add the SuperTokens interceptor + + Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. + + + + + + +```swift title="Mobile" option="mobile-frameworks:ios" +import Foundation +import SuperTokensIOS +import Alamofire + +fileprivate class NetworkManager { + func setupSuperTokensInterceptor() { + let configuration = URLSessionConfiguration.af.default + configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) + let session = Session(configuration: configuration) + + // Use session when making network requests + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/dio.dart'; +import 'package:dio/dio.dart'; + +void setup() { + Dio dio = Dio(); // Create a Dio instance. + dio.addSupertokensInterceptor(); +} +``` + + + + + + + + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + ###### Making network requests + + You can make requests as you normally would with `dio`. + + + + + + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/dio.dart'; +import 'package:dio/dio.dart'; + +void setup() { + Dio dio = Dio( + // Provide your config here + ); + dio.addSupertokensInterceptor(); + + var response = dio.get("http://localhost:3001/api"); + // handle response +} +``` + + + + + + + + + + + :::note[By default our mobile SDKs use a bearer token in the Authorization header to provide credentials.] + ::: + + + + + +##### Without the Frontend SDK + +:::warning[We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time.] +::: + +In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. + +For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. + + + + + +###### During the Login Action + +You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". + +The login API returns the following headers: +- `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. +- `front-token` header: This contains information about the access token: + - The userID + - The expiry time of the access token + - The payload added by you in the access token. + + Here is the structure of the token: + ```tsx + let frontTokenFromRequestHeader = "..."; + let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); + console.log(frontTokenDecoded); + /* + { + ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required + uid: "....", // user ID + up: { + sub: "..", + iat: .., + ... // other access token payload + } + } + + */ + ``` + + This token is mainly used for cookie-based authentication because you don't have access to the actual access token on the frontend. You may still want to read its payload, for example to adjust the UI based on the user's role. The token is not signed and must not be used for authorization. If you cache it, treat its contents as untrusted and clear it when the session ends. + +- `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. + +###### When You Make Network Requests to Protected APIs + +The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: +- `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. +- `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. +- For cross-origin browser requests, set the Fetch `credentials` request option to `"include"` (or the equivalent option in your HTTP library). `credentials` is not an HTTP header and does not accept `true` in Fetch. + +An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: +- `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. +- `front-token`: This should be read and saved by you in the same way as it's being done during login. + +###### Handling session refreshing + +If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. + +You can call the refresh API as follows: + + +```bash +curl --location --request POST '/auth/session/refresh' \ +--header 'Cookie: sRefreshToken=...' +``` + +:::note[You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in.] +- The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. +::: + + +The result of a session refresh is either: +- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. +- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. + + + + + +###### During the Login Action + +You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". + +The login API returns the following headers: +- `st-access-token`: This contains the current access token associated with the session. +- `st-refresh-token`: This contains the current refresh token associated with the session. + +Do not persist these tokens in browser `localStorage`, because injected scripts can read them. Prefer the Web SDK's cookie-based mode for browsers. Native applications should use platform-provided secure storage. If you manually use header-based authentication in a browser, keep tokens in memory and account for the session ending when the page reloads. + +###### When You Make Network Requests to Protected APIs + +You need to add the following headers to request: +- `authorization: Bearer {access-token}` +- Header-based requests do not require the Fetch API's `credentials` option unless the request also relies on cookies or HTTP authentication. + +An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` + +These should be read and saved by you in the same way as it's being done during login. + +###### Handling session refreshing + +If a protected API returns `401`, attempt to refresh the session once before retrying the request. A `401` can have causes other than access-token expiry, so do not retry indefinitely. + +You can call the refresh API as follows: + + +```bash +curl --location --request POST '/auth/session/refresh' \ +--header 'authorization: Bearer {refresh-token}' +``` + + +The result of a session refresh is either: +- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. +- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. + + + + + + +#### 1.5 Protect frontend routes + +You can use the `doesSessionExist` function to check if a session exists. + + + +```tsx title="Web" option="install-method:npm" +import Session from "supertokens-web-js/recipe/session"; + +async function doesSessionExist() { + if (await Session.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +async function doesSessionExist() { + if (await SuperTokens.doesSessionExist()) { + // user is logged in + } else { + // user has not logged in yet + } +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + fun doesSessionExist() { + if (SuperTokens.doesSessionExist(this.applicationContext)) { + // user is logged in + } else { + // user has not logged in yet + } + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func doesSessionExist() { + if SuperTokens.doesSessionExist() { + // User is logged in + } else { + // User is not logged in + } + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +Future doesSessionExist() async { + return await SuperTokens.doesSessionExist(); +} +``` + + + + +#### 1.6 Add a sign-out action + +The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. + + + +```tsx title="Web" option="install-method:npm" +import Session from "supertokens-web-js/recipe/session"; + +async function logout() { + await Session.signOut(); + window.location.href = "/auth"; // or to wherever your logic page is +} +``` + +```tsx title="Mobile" option="mobile-frameworks:reactnative" +import SuperTokens from "supertokens-react-native"; + +async function logout() { + await SuperTokens.signOut(); + // navigate to the login screen.. +} +``` + +```kotlin title="Mobile" option="mobile-frameworks:android" +import android.app.Application +import com.supertokens.session.SuperTokens + +class MainApplication: Application() { + fun logout() { + SuperTokens.signOut(this); + // navigate to the login screen.. + } +} +``` + +```swift title="Mobile" option="mobile-frameworks:ios" +import UIKit +import SuperTokensIOS + +fileprivate class ViewController: UIViewController { + func signOut() { + SuperTokens.signOut(completionHandler: { + error in + + if error != nil { + // handle error + } else { + // Signed out successfully + } + }) + } +} +``` + +```dart title="Mobile" option="mobile-frameworks:flutter" +import 'package:supertokens_flutter/supertokens.dart'; + +Future signOut() async { + await SuperTokens.signOut( + completionHandler: (error) { + // handle error if any + } + ); +} +``` + + + +- On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. +- The `signOut` function calls the sign out API exposed by the session recipe on the backend. +- If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session. + + + + +### 2. Integrate the backend SDK + +Let's go through the changes required so that your backend can expose the **SuperTokens** authentication features. + +2.1 Install the backend SDK + +Run the following command in your terminal to install the package. + + +```bash title="Node.js" option="package-managers:npm" +npm i -s supertokens-node +``` + +```bash title="Node.js" option="package-managers:yarn" +yarn add supertokens-node +``` + +```bash title="Node.js" option="package-managers:pnpm" +pnpm add supertokens-node +``` + +```bash title="Node.js" option="package-managers:bun" +bun add supertokens-node +``` + +```bash title="Go" +go get github.com/supertokens/supertokens-golang +``` + +```bash title="Python" +pip install supertokens-python +``` + + +:::info[Official backend SDKs are available for **Node.js**, **Python**, and **Go**.] +For other languages, create a separate authentication service. Our [other frameworks guide](/references/backend-sdks/other-frameworks) explains this approach. + +::: + +#### 2.2 Initialize the backend SDK + + +You will have to initialize the **Backend SDK** alongside the code that starts your server. +The init call will include [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. + + +```tsx title="Node.js" option="node-frameworks:express" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "express", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "hapi", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "fastify", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "koa", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import supertokens from "supertokens-node"; +import Session from "supertokens-node/recipe/session"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; + +supertokens.init({ + framework: "loopback", + supertokens: { + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + connectionURI: "https://try.supertokens.io", + // apiKey: + }, + appInfo: { + // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration + appName: "", + apiDomain: "", + websiteDomain: "", + apiBasePath: "/auth", + websiteBasePath: "/auth", + }, + recipeList: [ + EmailPassword.init(), // initializes signin / sign up features + Session.init(), // initializes session features + ], +}); +``` + +```go title="Go" + import ( + "github.com/supertokens/supertokens-golang/recipe/emailpassword" + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/supertokens" + ) + + func main() { + apiBasePath := "/auth" + websiteBasePath := "/auth" + err := supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + // We use try.supertokens for demo purposes. + // At the end of the tutorial we will show you how to create + // your own SuperTokens core instance and then update your config. + ConnectionURI: "https://try.supertokens.io", + // APIKey: + }, + AppInfo: supertokens.AppInfo{ + AppName: "", + APIDomain: "", + WebsiteDomain: "", + APIBasePath: &apiBasePath, + WebsiteBasePath: &websiteBasePath, + }, + RecipeList: []supertokens.Recipe{ + emailpassword.Init(nil), + session.Init(nil), + }, + }) + + if err != nil { + panic(err.Error()) + } + } +``` + +```python title="Python" option="python-frameworks:fastapi" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='fastapi', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ], + mode='asgi' # use wsgi if you are running using gunicorn +) +``` + +```python title="Python" option="python-frameworks:flask" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='flask', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ] +) +``` + +```python title="Python" option="python-frameworks:django" +from supertokens_python import init, InputAppInfo, SupertokensConfig +from supertokens_python.recipe import emailpassword, session + +init( + app_info=InputAppInfo( + app_name="", + api_domain="", + website_domain="", + api_base_path="/auth", + website_base_path="/auth" + ), + supertokens_config=SupertokensConfig( + # We use try.supertokens for demo purposes. + # At the end of the tutorial we will show you how to create + # your own SuperTokens core instance and then update your config. + connection_uri="https://try.supertokens.io", + # api_key: + ), + framework='django', + recipe_list=[ + session.init(), # initializes session features + emailpassword.init() + ], + mode='asgi' # use wsgi if you are running django server in sync mode +) +``` + + + +:::info[Multiple frontend domains] +To handle clients from different domains with the same SuperTokens instance, use the `origin` property in the `appInfo` object instead of `websiteDomain`. +The property accepts a function that receives the original request as an input and should return a valid domain. +Make sure to whitelist all the domains during CORS configuration. + +Keep in mind that with this setup, each frontend application will not share authentication sessions. +Users will have to authenticate separately for each domain. +To configure a shared authentication experience between multiple services check the [Unified Login](/authentication/unified-login/introduction) documentation. +::: + + +#### 2.3 Add the SuperTokens APIs and configure CORS + +Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. +Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. + + + + + Register the `plugin`. + + Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. + + + :::note[Add the `middleware` BEFORE all your routes.] + ::: + + + :::note[Add the `middleware` BEFORE all your routes.] + ::: + + + + + Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. + + + + + Use the `Middleware` (**BEFORE all your routes**) and the `get_all_cors_headers()` functions as shown below. + + + - Use the `Middleware` (**BEFORE all your routes and after calling init function**) and the `get_all_cors_headers()` functions as shown below. + - Add a route to catch all paths and return a 404. This is needed because if we don't add this, then OPTIONS request for the APIs exposed by the `Middleware` will return a `404`. + + + Configure Django CORS + + Use the `Middleware` and the `get_all_cors_headers()` functions as shown below in your `settings.py`. + + + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express from "express"; +import cors from "cors"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/express"; + +let app = express(); + +app.use( + cors({ + origin: "", + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }), +); + +// IMPORTANT: CORS should be before the below line. +app.use(middleware()); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import Hapi from "@hapi/hapi"; +import supertokens from "supertokens-node"; +import { plugin } from "supertokens-node/framework/hapi"; + +let server = Hapi.server({ + port: 8000, + routes: { + cors: { + origin: [""], + additionalHeaders: [...supertokens.getAllCORSHeaders()], + credentials: true, + }, + }, +}); + +(async () => { + await server.register(plugin); + + await server.start(); +})(); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import cors from "@fastify/cors"; +import supertokens from "supertokens-node"; +import { plugin } from "supertokens-node/framework/fastify"; +import formDataPlugin from "@fastify/formbody"; + +import fastifyImport from "fastify"; + +let fastify = fastifyImport(); + +// ...other middlewares +fastify.register(cors, { + origin: "", + allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], + credentials: true, +}); + +(async () => { + await fastify.register(formDataPlugin); + await fastify.register(plugin); + + await fastify.listen({ port: 8000 }); +})(); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import Koa from "koa"; +import cors from "@koa/cors"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/koa"; + +let app = new Koa(); + +app.use( + cors({ + origin: "", + allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }), +); + +app.use(middleware()); + +// ...your API routes +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import { RestApplication } from "@loopback/rest"; +import supertokens from "supertokens-node"; +import { middleware } from "supertokens-node/framework/loopback"; + +let app = new RestApplication({ + rest: { + cors: { + origin: "", + allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], + credentials: true, + }, + }, +}); + +app.middleware(middleware); + +// ...your API routes +``` + +```go title="Go" option="go-frameworks:http" +import ( + "net/http" + "strings" + + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + http.ListenAndServe("SERVER ADDRESS", corsMiddleware( + supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, + r *http.Request) { + // TODO: Handle your APIs.. + + })))) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { + response.Header().Set("Access-Control-Allow-Origin", "") + response.Header().Set("Access-Control-Allow-Credentials", "true") + if r.Method == "OPTIONS" { + // we add content-type + other headers used by SuperTokens + response.Header().Set("Access-Control-Allow-Headers", + strings.Join(append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...), ",")) + response.Header().Set("Access-Control-Allow-Methods", "*") + response.Write([]byte("")) + } else { + next.ServeHTTP(response, r) + } + }) +} +``` + +```go title="Go" option="go-frameworks:gin" +import ( + "net/http" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + router := gin.New() + + // CORS + router.Use(cors.New(cors.Config{ + AllowOrigins: []string{""}, + AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}, + AllowHeaders: append([]string{"content-type"}, + supertokens.GetAllCORSHeaders()...), + AllowCredentials: true, + })) + + // Adding the SuperTokens middleware + router.Use(func(c *gin.Context) { + supertokens.Middleware(http.HandlerFunc( + func(rw http.ResponseWriter, r *http.Request) { + c.Next() + })).ServeHTTP(c.Writer, c.Request) + // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly + c.Abort() + }) + + // Add APIs and start server +} +``` + +```go title="Go" option="go-frameworks:chi" +import ( + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + r := chi.NewRouter() + + // CORS + r.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{""}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...), + AllowCredentials: true, + })) + + // SuperTokens Middleware + r.Use(supertokens.Middleware) + + // Add APIs and start server +} +``` + +```go title="Go" option="go-frameworks:mux" +import ( + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/supertokens/supertokens-golang/supertokens" +) + +func main() { + // SuperTokens init... + + // TODO: Add APIs + + router := mux.NewRouter() + + // Adding handlers.CORS(options)(supertokens.Middleware(router))) + http.ListenAndServe("SERVER ADDRESS", handlers.CORS( + handlers.AllowedHeaders(append([]string{"Content-Type"}, + supertokens.GetAllCORSHeaders()...)), + handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), + handlers.AllowedOrigins([]string{""}), + handlers.AllowCredentials(), + )(supertokens.Middleware(router))) +} +``` + +```python title="Python" option="python-frameworks:fastapi" +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware + +from supertokens_python import get_all_cors_headers +from supertokens_python.framework.fastapi import get_middleware + +app = FastAPI() +app.add_middleware(get_middleware()) + +# TODO: Add APIs + +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "" + ], + allow_credentials=True, + allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +# TODO: start server +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" +from supertokens_python import get_all_cors_headers +from flask import Flask, abort +from flask_cors import CORS +from supertokens_python.framework.flask import Middleware + +app = Flask(__name__) +Middleware(app) + +# TODO: Add APIs + +CORS( + app=app, + origins=[ + "" + ], + supports_credentials=True, + allow_headers=["Content-Type"] + get_all_cors_headers(), +) + +# This is required since if this is not there, then OPTIONS requests for +# the APIs exposed by the supertokens' Middleware will return a 404 +@app.route('/', defaults={'u_path': ''}) +@app.route('/') +def catch_all(u_path: str): + abort(404) + +# TODO: start server +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" +from typing import List + +from corsheaders.defaults import default_headers + +from supertokens_python import get_all_cors_headers + +CORS_ORIGIN_WHITELIST = [ + "" +] + +CORS_ALLOW_CREDENTIALS = True + +CORS_ALLOWED_ORIGINS = [ + "" +] + +CORS_ALLOW_HEADERS: List[str] = list(default_headers) + [ + "Content-Type" +] + get_all_cors_headers() + +INSTALLED_APPS = [ + 'corsheaders', + 'supertokens_python' +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + ..., + 'supertokens_python.framework.django.django_middleware.middleware', +] +# TODO: start server +``` + + +You can review all the endpoints that are added through the use of **SuperTokens** by visiting the [API Specs](https://app.swaggerhub.com/apis/supertokens/FDI). + +#### 2.4 Add the SuperTokens error handler + +Depending on the language and framework that you are using, you might need to add a custom error handler to your server. +The handler will catch all the authentication related errors and return proper HTTP responses that can be parsed by the frontend SDKs. + + + + + + No additional `errorHandler` is required. + + Add the `errorHandler` **Before all your routes and plugin registration** + + No additional `errorHandler` is required. + No additional `errorHandler` is required. + + + + :::info[You can skip this step] + ::: + + + :::info[You can skip this step] + ::: + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express, { Request, Response, NextFunction } from "express"; +import { errorHandler } from "supertokens-node/framework/express"; + +let app = express(); + +// ...your API routes + +// Add this AFTER all your routes +app.use(errorHandler()); + +// your own error handler +app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { + /* ... */ +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import Fastify from "fastify"; +import { errorHandler } from "supertokens-node/framework/fastify"; + +let fastify = Fastify(); + +fastify.setErrorHandler(errorHandler()); + +// ...your API routes +``` + + +#### 2.5 Secure application routes + +Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. + + + + For your APIs that require a user to be logged in, use the `verifySession` middleware. + + + For your APIs that require a user to be logged in, use the `VerifySession` middleware. + + + For your APIs that require a user to be logged in, use the `verify_session` middleware. + + + + +```tsx title="Node.js" option="node-frameworks:express" +import express from "express"; +import { verifySession } from "supertokens-node/recipe/session/framework/express"; +import { SessionRequest } from "supertokens-node/framework/express"; + +let app = express(); + +app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //.... +}); +``` + +```tsx title="Node.js" option="node-frameworks:hapi" +import Hapi from "@hapi/hapi"; +import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; +import { SessionRequest } from "supertokens-node/framework/hapi"; + +let server = Hapi.server({ port: 8000 }); + +server.route({ + path: "/like-comment", + method: "post", + options: { + pre: [ + { + method: verifySession(), + }, + ], + }, + handler: async (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //... + }, +}); +``` + +```tsx title="Node.js" option="node-frameworks:fastify" +import Fastify from "fastify"; +import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { SessionRequest } from "supertokens-node/framework/fastify"; + +let fastify = Fastify(); + +fastify.post( + "/like-comment", + { + preHandler: verifySession(), + }, + (req: SessionRequest, res) => { + let userId = req.session!.getUserId(); + //.... + }, +); +``` + +```tsx title="Node.js" option="node-frameworks:koa" +import KoaRouter from "koa-router"; +import { verifySession } from "supertokens-node/recipe/session/framework/koa"; +import { SessionContext } from "supertokens-node/framework/koa"; + +let router = new KoaRouter(); + +router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { + let userId = ctx.session!.getUserId(); + //.... +}); +``` + +```tsx title="Node.js" option="node-frameworks:loopback" +import { inject, intercept } from "@loopback/core"; +import { RestBindings, MiddlewareContext, post, response } from "@loopback/rest"; +import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; +import { SessionContext } from "supertokens-node/framework/loopback"; + +class LikeComment { + constructor(@inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext) {} + @post("/like-comment") + @intercept(verifySession()) + @response(200) + handler() { + let userId = (this.ctx as SessionContext).session!.getUserId(); + //.... + } +} +``` + +```go title="Go" option="go-frameworks:http" +import ( + "fmt" + "net/http" + + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // Wrap the API handler in session.VerifySession + session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) + }) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:gin" +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/supertokens/supertokens-golang/recipe/session" + "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" +) + +func main() { + router := gin.New() + + // Wrap the API handler in session.VerifySession + router.POST("/likecomment", verifySession(nil), likeCommentAPI) +} + +// This is a function that wraps the supertokens verification function +// to work the gin +func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc { + return func(c *gin.Context) { + session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) { + c.Request = c.Request.WithContext(r.Context()) + c.Next() + })(c.Writer, c.Request) + // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly + c.Abort() + } +} + +func likeCommentAPI(c *gin.Context) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:chi" +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi" + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + r := chi.NewRouter() + + // Wrap the API handler in session.VerifySession + r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```go title="Go" option="go-frameworks:mux" +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/supertokens/supertokens-golang/recipe/session" +) + +func main() { + router := mux.NewRouter() + + // Wrap the API handler in session.VerifySession + router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) +} + +func likeCommentAPI(w http.ResponseWriter, r *http.Request) { + // retrieve the session object as shown below + sessionContainer := session.GetSessionFromRequestContext(r.Context()) + + userID := sessionContainer.GetUserID() + + fmt.Println(userID) +} +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:fastapi" +from fastapi import Depends + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.fastapi import verify_session + + +@app.post('/like_comment') +async def like_comment(session: SessionContainer = Depends(verify_session())): + user_id = session.get_user_id() + + print(user_id) +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:flask" +from flask import g + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.flask import verify_session + + +@app.route('/update-jwt', methods=['POST']) +@verify_session() +def like_comment(): + session: SessionContainer = g.supertokens + + user_id = session.get_user_id() + + print(user_id) +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" option="python-frameworks:django" +from typing import cast + +from django.http import HttpRequest + +from supertokens_python.recipe.session import SessionContainer +from supertokens_python.recipe.session.framework.django.asyncio import verify_session + + +@verify_session() +async def like_comment(request: HttpRequest): + session: SessionContainer = cast(SessionContainer, request.supertokens) + + user_id = session.get_user_id() + + print(user_id) +``` + + +The middleware function returns a `401` to the frontend if a session doesn't exist, or if the access token has expired, in which case, our frontend SDK automatically refreshes the session. + +In case of successful session verification, you get access to a `session` object using which you can get the user's ID, or manipulate the session information. + +### 3. Configure the Core Service + +If you have signed up and deployed a SuperTokens environment already, you can skip this step. +Otherwise, please follow these instructions to use the correct **SuperTokens Core** instance in your application. + +The steps show you how to connect to a **SuperTokens Managed Service Environment**. +If you want to self host the core instance please check the [following guide](/deployment/self-host-supertokens). + + +#### 3.1 Sign up for a SuperTokens account + +Open this [page](https://supertokens.com/auth) in order to access the account creation page. +Select the account that you want to use and wait for the action to complete. + +#### 3.2 Create a deployment + +After signing in, open the SuperTokens dashboard and select **Managed**. Enter a name for the deployment, select the region closest to your backend services, and click **Deploy Core**. + +Our internal service will deploy a separate environment based on your selection. +After this process is complete, open the new deployment from the list. + +:::info[The initial setup flow only configures a development environment.] +In order to use SuperTokens in production, you will have to create a separate deployment. + +::: + +#### 3.3 Connect the backend SDK with SuperTokens + +In the SuperTokens dashboard, open the newly created deployment and select **Overview**. In **Connection Information**, copy the **Connection URI** and one of the **API Keys**, then use them as `connectionURI` and `apiKey` in your backend SDK configuration. If no suitable key exists, click **Generate Key** to create one. + + + +```tsx title="Node.js" +import supertokens from "supertokens-node"; + +supertokens.init({ + supertokens: { + connectionURI: "", + apiKey: "", + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [], +}); +``` + +```go title="Go" +import "github.com/supertokens/supertokens-golang/supertokens" + +func main() { + supertokens.Init(supertokens.TypeInput{ + Supertokens: &supertokens.ConnectionInfo{ + ConnectionURI: "", + APIKey: "", + }, + }) +} + +``` + +```python check=false reason="Requires surrounding quickstart application context" title="Python" +from supertokens_python import init, InputAppInfo, SupertokensConfig + +init( + app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), + supertokens_config=SupertokensConfig( + connection_uri='', + api_key='' + ), + framework='...', + recipe_list=[ + #... + ] +) +``` + + +## Next steps + + +Review this repository's SuperTokens integration for production readiness. Inspect Core deployment configuration, API keys, environment separation, HTTPS, secret handling, session security, CORS, cookies, email or SMS delivery, rate limits, logging, and error handling. Check that frontend and backend recipes match and that protected routes are actually protected. Run the relevant tests, typechecks, and build. Report findings by severity with file references, then make only safe fixes that are clearly required. + + +Now that you have completed the quickstart, continue configuring SuperTokens for your application's authentication and authorization requirements. + + + + Add passwordless, social, enterprise, or machine-to-machine authentication. + + + Verify user email addresses during sign-up. + + + Add more authentication factors to your sign-in process. + + + Configure session security, storage, and advanced workflows. + + + Manage users through the SuperTokens Dashboard. + + + Run SuperTokens as a managed service or inside your infrastructure. + + diff --git a/docs/quickstart/_blocks/angular-app-component.mdx b/docs/quickstart/_blocks/angular-app-component.mdx deleted file mode 100644 index ce6992ae3d..0000000000 --- a/docs/quickstart/_blocks/angular-app-component.mdx +++ /dev/null @@ -1,13 +0,0 @@ - ```tsx title="/app/app.component.ts" - import SuperTokens from "supertokens-web-js"; - import Session from "supertokens-web-js/recipe/session"; - - SuperTokens.init({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - }, - recipeList: [Session.init()], - }); - ``` diff --git a/docs/quickstart/_blocks/angular-auth-component.mdx b/docs/quickstart/_blocks/angular-auth-component.mdx deleted file mode 100644 index 59140a8006..0000000000 --- a/docs/quickstart/_blocks/angular-auth-component.mdx +++ /dev/null @@ -1,59 +0,0 @@ - ```tsx title="/app/auth/auth.component.ts" - import { init as supertokensUIInit } from "supertokens-auth-react-script"; - import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - import { - Component, - OnDestroy, - AfterViewInit, - Renderer2, - Inject, - } from "@angular/core"; - import { DOCUMENT } from "@angular/common"; - - @Component({ - selector: "app-auth", - template: '
', - }) - export class AuthComponent implements OnDestroy, AfterViewInit { - constructor( - private renderer: Renderer2, - @Inject(DOCUMENT) private document: Document, - ) {} - - ngAfterViewInit() { - this.loadScript("^{prebuiltUIVersion}"); - } - - ngOnDestroy() { - // Remove the script when the component is destroyed - const script = this.document.getElementById("supertokens-script"); - if (script) { - script.remove(); - } - } - - private loadScript(src: string) { - const script = this.renderer.createElement("script"); - script.type = "text/javascript"; - script.src = src; - script.id = "supertokens-script"; - script.onload = () => { - supertokensUIInit({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - supertokensUIEmailPassword.init(), - supertokensUISession.init(), - ], - }); - }; - this.renderer.appendChild(this.document.body, script); - } - } - ``` diff --git a/docs/quickstart/_blocks/angular-routing.mdx b/docs/quickstart/_blocks/angular-routing.mdx deleted file mode 100644 index c93b225c02..0000000000 --- a/docs/quickstart/_blocks/angular-routing.mdx +++ /dev/null @@ -1,26 +0,0 @@ -```tsx title="/app/app-routing.module.ts" -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; - -const routes: Routes = [ - // highlight-start - { - path: "^{appInfo.websiteBasePath_withoutForwardSlash}", - // @ts-ignore - loadChildren: () => import("./auth/auth.module").then((m) => m.AuthModule), - }, - - { - path: "**", - // @ts-ignore - loadChildren: () => import("./home/home.module").then((m) => m.HomeModule), - }, - // highlight-end -]; - -@NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule], -}) -export class AppRoutingModule {} -``` diff --git a/docs/quickstart/_blocks/curl-sign-in.mdx b/docs/quickstart/_blocks/curl-sign-in.mdx deleted file mode 100644 index 842e57b5ce..0000000000 --- a/docs/quickstart/_blocks/curl-sign-in.mdx +++ /dev/null @@ -1,14 +0,0 @@ - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signin' \ ---header 'Content-Type: application/json; charset=utf-8' \ ---data-raw '{ - "formFields": [{ - "id": "email", - "value": "john@example.com" - }, { - "id": "password", - "value": "somePassword123" - }] -}' -``` diff --git a/docs/quickstart/_blocks/flutter-custom-client-session.mdx b/docs/quickstart/_blocks/flutter-custom-client-session.mdx deleted file mode 100644 index 6856a9b4ad..0000000000 --- a/docs/quickstart/_blocks/flutter-custom-client-session.mdx +++ /dev/null @@ -1,17 +0,0 @@ - -```dart -// Import http from the SuperTokens package -import 'package:supertokens_flutter/http.dart' as http; - -Future makeRequest() async { - Uri uri = Uri.parse("http://localhost:3001/api"); - - // Initialise your custom client - var customClient = http.Client(); - // provide your custom client to SuperTokens - var httpClient = http.Client(client: customClient); - - var response = await httpClient.get(uri); - // handle response -} -``` diff --git a/docs/quickstart/_blocks/flutter-dio-request-example.mdx b/docs/quickstart/_blocks/flutter-dio-request-example.mdx deleted file mode 100644 index f026fc0781..0000000000 --- a/docs/quickstart/_blocks/flutter-dio-request-example.mdx +++ /dev/null @@ -1,15 +0,0 @@ - -```dart -import 'package:supertokens_flutter/dio.dart'; -import 'package:dio/dio.dart'; - -void setup() { - Dio dio = Dio( - // Provide your config here - ); - dio.addSupertokensInterceptor(); - - var response = dio.get("http://localhost:3001/api"); - // handle response -} -``` diff --git a/docs/quickstart/_blocks/flutter-dio-session.mdx b/docs/quickstart/_blocks/flutter-dio-session.mdx deleted file mode 100644 index f5ab2aefa4..0000000000 --- a/docs/quickstart/_blocks/flutter-dio-session.mdx +++ /dev/null @@ -1,10 +0,0 @@ - -```dart -import 'package:supertokens_flutter/dio.dart'; -import 'package:dio/dio.dart'; - -void setup() { - Dio dio = Dio(); // Create a Dio instance. - dio.addSupertokensInterceptor(); -} -``` diff --git a/docs/quickstart/_blocks/flutter-http-session.mdx b/docs/quickstart/_blocks/flutter-http-session.mdx deleted file mode 100644 index 56b8cbde21..0000000000 --- a/docs/quickstart/_blocks/flutter-http-session.mdx +++ /dev/null @@ -1,11 +0,0 @@ - -```dart -// Import http from the SuperTokens package -import 'package:supertokens_flutter/http.dart' as http; - -Future makeRequest() async { - Uri uri = Uri.parse("http://localhost:3001/api"); - var response = await http.get(uri); - // handle response -} -``` diff --git a/docs/quickstart/_blocks/go-sdk-init.mdx b/docs/quickstart/_blocks/go-sdk-init.mdx deleted file mode 100644 index 8ccd3a1814..0000000000 --- a/docs/quickstart/_blocks/go-sdk-init.mdx +++ /dev/null @@ -1,37 +0,0 @@ - - ```go - import ( - "github.com/supertokens/supertokens-golang/recipe/emailpassword" - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/supertokens" - ) - - func main() { - apiBasePath := "^{appInfo.apiBasePath}" - websiteBasePath := "^{appInfo.websiteBasePath}" - err := supertokens.Init(supertokens.TypeInput{ - Supertokens: &supertokens.ConnectionInfo{ - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - ConnectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // APIKey: - }, - AppInfo: supertokens.AppInfo{ - AppName: "^{appInfo.appName}", - APIDomain: "^{appInfo.apiDomain}", - WebsiteDomain: "^{appInfo.websiteDomain}", - APIBasePath: &apiBasePath, - WebsiteBasePath: &websiteBasePath, - }, - RecipeList: []supertokens.Recipe{ - emailpassword.Init(nil), - session.Init(nil), - }, - }) - - if err != nil { - panic(err.Error()) - } - } - ``` diff --git a/docs/quickstart/_blocks/kotlin-http-urlconnection-session.mdx b/docs/quickstart/_blocks/kotlin-http-urlconnection-session.mdx deleted file mode 100644 index 8ab7cbbf46..0000000000 --- a/docs/quickstart/_blocks/kotlin-http-urlconnection-session.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens -import com.supertokens.session.SuperTokensHttpURLConnection -import com.supertokens.session.SuperTokensPersistentCookieStore -import java.net.URL -import java.net.HttpURLConnection - -class MainApplication: Application() { - override fun onCreate() { - super.onCreate() - // TODO: Make sure to call SuperTokens.init - } - - fun makeRequest() { - val url = URL("") - val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { - override fun doAction(con: HttpURLConnection?) { - // TODO: Use `con` to set request method, headers etc - } - }) - - // Handle response using connection object, for example: - if (connection.responseCode == 200) { - // TODO: implement - } - } -} -``` diff --git a/docs/quickstart/_blocks/kotlin-okhttp-session.mdx b/docs/quickstart/_blocks/kotlin-okhttp-session.mdx deleted file mode 100644 index 02315079da..0000000000 --- a/docs/quickstart/_blocks/kotlin-okhttp-session.mdx +++ /dev/null @@ -1,31 +0,0 @@ - -```kotlin -import android.content.Context -import com.supertokens.session.SuperTokens -import com.supertokens.session.SuperTokensInterceptor -import okhttp3.OkHttpClient -import retrofit2.Retrofit - -class NetworkManager { - fun getClient(context: Context): OkHttpClient { - val clientBuilder = OkHttpClient.Builder() - clientBuilder.addInterceptor(SuperTokensInterceptor()) - // TODO: Make sure to call SuperTokens.init - - val client = clientBuilder.build() - - // REQUIRED FOR RETROFIT ONLY - val instance = Retrofit.Builder() - .baseUrl("") - .client(client) - .build() - - return client - } - - fun makeRequest(context: Context) { - val client = getClient(context) - // Use client to make requests normally - } -} -``` diff --git a/docs/quickstart/_blocks/kotlin-sdk-init.mdx b/docs/quickstart/_blocks/kotlin-sdk-init.mdx deleted file mode 100644 index f8ee5ff79f..0000000000 --- a/docs/quickstart/_blocks/kotlin-sdk-init.mdx +++ /dev/null @@ -1,17 +0,0 @@ - - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens - -class MainApplication: Application() { - override fun onCreate() { - super.onCreate() - - SuperTokens.Builder(this, "^{appInfo.apiDomain}") - .apiBasePath("^{appInfo.apiBasePath}") - .build() - } -} -``` - diff --git a/docs/quickstart/_blocks/node-express-sdk-init.mdx b/docs/quickstart/_blocks/node-express-sdk-init.mdx deleted file mode 100644 index 3c8525d21d..0000000000 --- a/docs/quickstart/_blocks/node-express-sdk-init.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```tsx -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -import EmailPassword from "supertokens-node/recipe/emailpassword"; - -supertokens.init({ - framework: "express", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), // initializes signin / sign up features - Session.init() // initializes session features - ] -}); - -``` diff --git a/docs/quickstart/_blocks/node-fastify-sdk-init.mdx b/docs/quickstart/_blocks/node-fastify-sdk-init.mdx deleted file mode 100644 index d287da80c6..0000000000 --- a/docs/quickstart/_blocks/node-fastify-sdk-init.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```tsx -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -import EmailPassword from "supertokens-node/recipe/emailpassword"; - -supertokens.init({ - framework: "fastify", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), // initializes signin / sign up features - Session.init() // initializes session features - ] -}); -``` - diff --git a/docs/quickstart/_blocks/node-hapi-sdk-init.mdx b/docs/quickstart/_blocks/node-hapi-sdk-init.mdx deleted file mode 100644 index a42b175c99..0000000000 --- a/docs/quickstart/_blocks/node-hapi-sdk-init.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```tsx -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -import EmailPassword from "supertokens-node/recipe/emailpassword"; - -supertokens.init({ - framework: "hapi", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), // initializes signin / sign up features - Session.init() // initializes session features - ] -}); -``` - diff --git a/docs/quickstart/_blocks/node-koa-sdk-init.mdx b/docs/quickstart/_blocks/node-koa-sdk-init.mdx deleted file mode 100644 index 1fc76bec16..0000000000 --- a/docs/quickstart/_blocks/node-koa-sdk-init.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```tsx -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -import EmailPassword from "supertokens-node/recipe/emailpassword"; - -supertokens.init({ - framework: "koa", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), // initializes signin / sign up features - Session.init() // initializes session features - ] -}); -``` - diff --git a/docs/quickstart/_blocks/node-loopback-sdk-init.mdx b/docs/quickstart/_blocks/node-loopback-sdk-init.mdx deleted file mode 100644 index feaac7b93d..0000000000 --- a/docs/quickstart/_blocks/node-loopback-sdk-init.mdx +++ /dev/null @@ -1,30 +0,0 @@ - -```tsx -import supertokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; -import EmailPassword from "supertokens-node/recipe/emailpassword"; - -supertokens.init({ - framework: "loopback", - supertokens: { - // We use try.supertokens for demo purposes. - // At the end of the tutorial we will show you how to create - // your own SuperTokens core instance and then update your config. - connectionURI: "https://try.supertokens.io^{derived.appIdPathname}", - // apiKey: - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), // initializes signin / sign up features - Session.init() // initializes session features - ] -}); -``` - diff --git a/docs/quickstart/_blocks/python-django-sdk-init.mdx b/docs/quickstart/_blocks/python-django-sdk-init.mdx deleted file mode 100644 index e369ad9c95..0000000000 --- a/docs/quickstart/_blocks/python-django-sdk-init.mdx +++ /dev/null @@ -1,28 +0,0 @@ - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import emailpassword, session - -init( - app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" - ), - supertokens_config=SupertokensConfig( - # We use try.supertokens for demo purposes. - # At the end of the tutorial we will show you how to create - # your own SuperTokens core instance and then update your config. - connection_uri="https://try.supertokens.io^{derived.appIdPathname}", - # api_key: - ), - framework='django', - recipe_list=[ - session.init(), # initializes session features - emailpassword.init() - ], - mode='asgi' # use wsgi if you are running django server in sync mode -) -``` diff --git a/docs/quickstart/_blocks/python-fastapi-sdk-init.mdx b/docs/quickstart/_blocks/python-fastapi-sdk-init.mdx deleted file mode 100644 index 953ca6c4ff..0000000000 --- a/docs/quickstart/_blocks/python-fastapi-sdk-init.mdx +++ /dev/null @@ -1,28 +0,0 @@ - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import emailpassword, session - -init( - app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" - ), - supertokens_config=SupertokensConfig( - # We use try.supertokens for demo purposes. - # At the end of the tutorial we will show you how to create - # your own SuperTokens core instance and then update your config. - connection_uri="https://try.supertokens.io^{derived.appIdPathname}", - # api_key: - ), - framework='fastapi', - recipe_list=[ - session.init(), # initializes session features - emailpassword.init() - ], - mode='asgi' # use wsgi if you are running using gunicorn -) -``` diff --git a/docs/quickstart/_blocks/python-flask-sdk-init.mdx b/docs/quickstart/_blocks/python-flask-sdk-init.mdx deleted file mode 100644 index b36c3f16a2..0000000000 --- a/docs/quickstart/_blocks/python-flask-sdk-init.mdx +++ /dev/null @@ -1,27 +0,0 @@ - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig -from supertokens_python.recipe import emailpassword, session - -init( - app_info=InputAppInfo( - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}" - ), - supertokens_config=SupertokensConfig( - # We use try.supertokens for demo purposes. - # At the end of the tutorial we will show you how to create - # your own SuperTokens core instance and then update your config. - connection_uri="https://try.supertokens.io^{derived.appIdPathname}", - # api_key: - ), - framework='flask', - recipe_list=[ - session.init(), # initializes session features - emailpassword.init() - ] -) -``` diff --git a/docs/quickstart/_blocks/react-no-router.mdx b/docs/quickstart/_blocks/react-no-router.mdx deleted file mode 100644 index 438a479fa4..0000000000 --- a/docs/quickstart/_blocks/react-no-router.mdx +++ /dev/null @@ -1,23 +0,0 @@ - -```tsx -import React from 'react'; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; -import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui"; - -class App extends React.Component { - render() { - // highlight-start - if (canHandleRoute([EmailPasswordPreBuiltUI])) { - // This renders the login UI on the ^{appInfo.websiteBasePath} route - return getRoutingComponent([EmailPasswordPreBuiltUI]) - } - // highlight-end - - return ( - {/*Your app*/} - ); - } - -} -``` diff --git a/docs/quickstart/_blocks/react-router-v5.mdx b/docs/quickstart/_blocks/react-router-v5.mdx deleted file mode 100644 index 31f0433b52..0000000000 --- a/docs/quickstart/_blocks/react-router-v5.mdx +++ /dev/null @@ -1,34 +0,0 @@ - -```tsx -import React from 'react'; -import { - BrowserRouter, - Switch, - Route, - Link -} from "react-router-dom5"; - -// highlight-next-line -import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; -import * as reactRouterDom from "react-router-dom"; - - -class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the ^{appInfo.websiteBasePath} route*/} - // highlight-next-line - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } -} -``` diff --git a/docs/quickstart/_blocks/react-router-v6.mdx b/docs/quickstart/_blocks/react-router-v6.mdx deleted file mode 100644 index 8902af13af..0000000000 --- a/docs/quickstart/_blocks/react-router-v6.mdx +++ /dev/null @@ -1,33 +0,0 @@ - -```tsx -import React from 'react'; -import { - BrowserRouter, - Routes, - Route, - Link -} from "react-router-dom"; - -// highlight-next-line -import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; -import * as reactRouterDom from "react-router-dom"; - -class App extends React.Component { - render() { - return ( - - - - {/*This renders the login UI on the ^{appInfo.websiteBasePath} route*/} - // highlight-next-line - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [EmailPasswordPreBuiltUI])} - {/*Your app routes*/} - - - - ); - } -} -``` diff --git a/docs/quickstart/_blocks/react-sdk-init.mdx b/docs/quickstart/_blocks/react-sdk-init.mdx deleted file mode 100644 index 8676695ca0..0000000000 --- a/docs/quickstart/_blocks/react-sdk-init.mdx +++ /dev/null @@ -1,35 +0,0 @@ - -```tsx -import React from "react"; - -// highlight-start -import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react"; -import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; -import Session from "supertokens-auth-react/recipe/session"; - -SuperTokens.init({ - appInfo: { - // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [EmailPassword.init(), Session.init()], -}); -// highlight-end - -/* Your App */ -class App extends React.Component { - render() { - return ( - // highlight-next-line - - {/*Your app components*/} - // highlight-next-line - - ); - } -} -``` diff --git a/docs/quickstart/_blocks/swift-almorife.mdx b/docs/quickstart/_blocks/swift-almorife.mdx deleted file mode 100644 index 8d045b3652..0000000000 --- a/docs/quickstart/_blocks/swift-almorife.mdx +++ /dev/null @@ -1,18 +0,0 @@ - - -```swift -import Foundation -import SuperTokensIOS -import Alamofire - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - let configuration = URLSessionConfiguration.af.default - configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) - let session = Session(configuration: configuration) - - // Use session when making network requests - } -} -``` - diff --git a/docs/quickstart/_blocks/swift-sdk-init.mdx b/docs/quickstart/_blocks/swift-sdk-init.mdx deleted file mode 100644 index 791cb868ff..0000000000 --- a/docs/quickstart/_blocks/swift-sdk-init.mdx +++ /dev/null @@ -1,24 +0,0 @@ - -```swift -import UIKit -import SuperTokensIOS - -fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - do { - try SuperTokens.initialize( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}" - ) - } catch SuperTokensError.initError(let message) { - // TODO: Handle initialization error - } catch { - // Some other error - } - - return true - } - -} -``` diff --git a/docs/quickstart/_blocks/swift-url-session-instance.mdx b/docs/quickstart/_blocks/swift-url-session-instance.mdx deleted file mode 100644 index bbadd409f5..0000000000 --- a/docs/quickstart/_blocks/swift-url-session-instance.mdx +++ /dev/null @@ -1,15 +0,0 @@ - -```swift -import Foundation -import SuperTokensIOS - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [SuperTokensURLProtocol.self] - let session = URLSession(configuration: configuration) - - // Use session when making network requests - } -} -``` diff --git a/docs/quickstart/_blocks/swift-urlsession-shared.mdx b/docs/quickstart/_blocks/swift-urlsession-shared.mdx deleted file mode 100644 index 9a770c1103..0000000000 --- a/docs/quickstart/_blocks/swift-urlsession-shared.mdx +++ /dev/null @@ -1,11 +0,0 @@ - -```swift -import Foundation -import SuperTokensIOS - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - URLProtocol.registerClass(SuperTokensURLProtocol.self) - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-npm-check-email.mdx b/docs/quickstart/_blocks/vanilla-js-npm-check-email.mdx deleted file mode 100644 index 34c0c73d05..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-npm-check-email.mdx +++ /dev/null @@ -1,23 +0,0 @@ - -```tsx -import { doesEmailExist } from "supertokens-web-js/recipe/emailpassword"; - -async function checkEmail(email: string) { - try { - let response = await doesEmailExist({ - email - }); - - if (response.doesExist) { - window.alert("Email already exists. Please sign in instead") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-npm-sdk-init.mdx b/docs/quickstart/_blocks/vanilla-js-npm-sdk-init.mdx deleted file mode 100644 index 2c8f6fc6ff..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-npm-sdk-init.mdx +++ /dev/null @@ -1,18 +0,0 @@ - -```tsx -import SuperTokens from 'supertokens-web-js'; -import Session from 'supertokens-web-js/recipe/session'; -import EmailPassword from 'supertokens-web-js/recipe/emailpassword' - -SuperTokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - Session.init(), - EmailPassword.init(), - ], -}); -``` diff --git a/docs/quickstart/_blocks/vanilla-js-npm-sign-in.mdx b/docs/quickstart/_blocks/vanilla-js-npm-sign-in.mdx deleted file mode 100644 index bf459d8a1d..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-npm-sign-in.mdx +++ /dev/null @@ -1,45 +0,0 @@ - -```tsx -import { signIn } from "supertokens-web-js/recipe/emailpassword"; - -async function signInClicked(email: string, password: string) { - try { - let response = await signIn({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax). - window.alert(formField.error) - } - }) - } else if (response.status === "WRONG_CREDENTIALS_ERROR") { - window.alert("Email password combination is incorrect.") - } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in was not allowed. - window.alert(response.reason) - } else { - // sign in successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-npm-sign-up.mdx b/docs/quickstart/_blocks/vanilla-js-npm-sign-up.mdx deleted file mode 100644 index 22f35d6514..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-npm-sign-up.mdx +++ /dev/null @@ -1,49 +0,0 @@ - -```tsx -import { signUp } from "supertokens-web-js/recipe/emailpassword"; - -async function signUpClicked(email: string, password: string) { - try { - let response = await signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - // one of the input formFields failed validation - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax), - // or the email is not unique. - window.alert(formField.error) - } else if (formField.id === "password") { - // Password validation failed. - // Maybe it didn't match the password strength - window.alert(formField.error) - } - }) - } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign up was not allowed. - window.alert(response.reason) - } else { - // sign up successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-script-check-email.mdx b/docs/quickstart/_blocks/vanilla-js-script-check-email.mdx deleted file mode 100644 index 892669e59a..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-script-check-email.mdx +++ /dev/null @@ -1,22 +0,0 @@ - -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -async function checkEmail(email: string) { - try { - let response = await supertokensEmailPassword.doesEmailExist({ - email - }); - - if (response.doesExist) { - window.alert("Email already exists. Please sign in instead") - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-script-sdk-init.mdx b/docs/quickstart/_blocks/vanilla-js-script-sdk-init.mdx deleted file mode 100644 index 01906e7391..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-script-sdk-init.mdx +++ /dev/null @@ -1,17 +0,0 @@ - -```tsx -import supertokens from "supertokens-web-js-script"; -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensEmailPassword from 'supertokens-web-js-script/recipe/emailpassword' -supertokens.init({ - appInfo: { - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - appName: "...", - }, - recipeList: [ - supertokensSession.init(), - supertokensEmailPassword.init(), - ], -}); -``` diff --git a/docs/quickstart/_blocks/vanilla-js-script-sign-in.mdx b/docs/quickstart/_blocks/vanilla-js-script-sign-in.mdx deleted file mode 100644 index a061d41f34..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-script-sign-in.mdx +++ /dev/null @@ -1,45 +0,0 @@ - -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -async function signInClicked(email: string, password: string) { - try { - let response = await supertokensEmailPassword.signIn({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - // one of the input formFields failed validation - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax). - window.alert(formField.error) - } - }) - } else if (response.status === "WRONG_CREDENTIALS_ERROR") { - window.alert("Email password combination is incorrect.") - } else if (response.status === "SIGN_IN_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in was not allowed. - window.alert(response.reason) - } else { - // sign in successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vanilla-js-script-sign-up.mdx b/docs/quickstart/_blocks/vanilla-js-script-sign-up.mdx deleted file mode 100644 index ce7435d747..0000000000 --- a/docs/quickstart/_blocks/vanilla-js-script-sign-up.mdx +++ /dev/null @@ -1,48 +0,0 @@ - -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -async function signUpClicked(email: string, password: string) { - try { - let response = await supertokensEmailPassword.signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - // one of the input formFields failed validation - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax), - // or the email is not unique. - window.alert(formField.error) - } else if (formField.id === "password") { - // Password validation failed. - // Maybe it didn't match the password strength - window.alert(formField.error) - } - }) - } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in was not allowed. - window.alert(response.reason) - } else { - // sign up successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` diff --git a/docs/quickstart/_blocks/vue-auth-view.mdx b/docs/quickstart/_blocks/vue-auth-view.mdx deleted file mode 100644 index d0031c55bd..0000000000 --- a/docs/quickstart/_blocks/vue-auth-view.mdx +++ /dev/null @@ -1,50 +0,0 @@ - - ```tsx - import {init as supertokensUIInit} from "supertokens-auth-react-script"; - import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; - import supertokensUISession from "supertokens-auth-react-script/recipe/session"; - - - - ``` diff --git a/docs/quickstart/_blocks/vue-main-file.mdx b/docs/quickstart/_blocks/vue-main-file.mdx deleted file mode 100644 index fcced61928..0000000000 --- a/docs/quickstart/_blocks/vue-main-file.mdx +++ /dev/null @@ -1,26 +0,0 @@ - - ```tsx title="/main.ts " - // @ts-ignore - import { createApp } from "vue"; - import SuperTokens from "supertokens-web-js"; - import Session from "supertokens-web-js/recipe/session"; - // @ts-ignore - import App from "./App.vue"; - // @ts-ignore - import router from "./router"; - - SuperTokens.init({ - appInfo: { - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - }, - recipeList: [Session.init()], - }); - - const app = createApp(App); - - app.use(router); - - app.mount("#app"); - ``` diff --git a/docs/quickstart/_blocks/vue-routing.mdx b/docs/quickstart/_blocks/vue-routing.mdx deleted file mode 100644 index 5b77a0892d..0000000000 --- a/docs/quickstart/_blocks/vue-routing.mdx +++ /dev/null @@ -1,29 +0,0 @@ - -```tsx title="/router/index.ts" -// @ts-ignore -import { createRouter, createWebHistory } from "vue-router"; -// @ts-ignore -import HomeView from "../views/HomeView.vue"; -// @ts-ignore -import AuthView from "../views/AuthView.vue"; - -const router = createRouter({ - // @ts-ignore - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: "/", - name: "home", - component: HomeView, - }, - { - path: "^{appInfo.websiteBasePath}/:pathMatch(.*)*", - name: "auth", - component: AuthView, - }, - ], -}); - -export default router; -``` - diff --git a/docs/quickstart/_category_.json b/docs/quickstart/_category_.json deleted file mode 100644 index 7590589946..0000000000 --- a/docs/quickstart/_category_.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "label": "Quickstart", - "collapsible": true, - "collapsed": false, - "customProps": { - "isMainCategory": true - }, - "position": 2 -} diff --git a/docs/quickstart/backend-setup.mdx b/docs/quickstart/backend-setup.mdx deleted file mode 100644 index 0845ed3296..0000000000 --- a/docs/quickstart/backend-setup.mdx +++ /dev/null @@ -1,1114 +0,0 @@ ---- -id: backend-setup -title: Backend Setup -hide_title: true -sidebar_position: 3 -description: >- - Set up SuperTokens authentication for your backend using Node.js, Python, or - Go SDKs. -page_type: tutorial -category: quickstart ---- - - -import NodeExpressSDKInit from "./_blocks/node-express-sdk-init.mdx"; -import NodeHapiSDKInit from "./_blocks/node-hapi-sdk-init.mdx"; -import NodeFastifySDKInit from "./_blocks/node-fastify-sdk-init.mdx"; -import NodeKoaSDKInit from "./_blocks/node-koa-sdk-init.mdx"; -import NodeLoopbackSDKInit from "./_blocks/node-loopback-sdk-init.mdx"; -import GoSDKInit from "./_blocks/go-sdk-init.mdx"; -import PythonFlaskSDKInit from "./_blocks/python-flask-sdk-init.mdx"; -import PythonDjangoSDKInit from "./_blocks/python-django-sdk-init.mdx"; -import PythonFastAPISDKInit from "./_blocks/python-fastapi-sdk-init.mdx"; - -# Backend Setup - -Let's go through the changes required so that your backend can expose the **SuperTokens** authentication features. - -## 1. Install the SDK - -Run the following command in your terminal to install the package. - - - - - -```bash -npm i -s supertokens-node -``` - - - - - -```bash -go get github.com/supertokens/supertokens-golang -``` - - - - -```bash -pip install supertokens-python -``` - - - - -:::info - -At the moment we only have SDKs for **Node.js**, **Python** and **Go**. -If you wish to use **SuperTokens** with other languages you will have to create a separate authentication service. -Please check [our guide](/docs/references/backend-sdks/other-frameworks) that shows you how to resolve this. - -::: - -## 2. Initialize the SDK - - - -You will have to initialize the **Backend SDK** alongside the code that starts your server. -The init call will include [configuration details](/docs/references/backend-sdks/reference#sdk-configuration) for your app, how the backend will connect to the **SuperTokens Core**, as well as the **Recipes** that will be used in your setup. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -:::info Multiple frontend domains -To handle clients from different domains with the same SuperTokens instance, use the `origin` property in the `appInfo` object instead of `websiteDomain`. -The property accepts a function that receives the original request as an input and should return a valid domain. -Make sure to whitelist all the domains during CORS configuration. - -Keep in mind that with this setup, each frontend application will not share authentication sessions. -Users will have to authenticate separately for each domain. -To configure a shared authentication experience between multiple services check the [Unified Login](/docs/authentication/unified-login/introduction) documentation. -::: - - -## 3. Add the SuperTokens APIs and Configure CORS - -Now that the SDK is initialized you need to expose the endpoints that will be used by the frontend SDKs. -Besides this, your server's CORS, Cross-Origin Resource Sharing, settings should be updated to allow the use of the authentication headers required by **SuperTokens**. - - - - - - - - - -:::important - -- Add the `middleware` BEFORE all your routes. -- Add the `cors` middleware BEFORE the SuperTokens middleware as shown below. -::: - - - -```tsx -import express from "express"; -import cors from "cors"; -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/express"; - -let app = express(); - -app.use( - cors({ - // highlight-start - origin: "^{appInfo.websiteDomain}", - allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - credentials: true, - // highlight-end - }), -); - -// IMPORTANT: CORS should be before the below line. -// highlight-next-line -app.use(middleware()); - -// ...your API routes -``` - - - - - - -Register the `plugin`. - - - -```tsx -import Hapi from "@hapi/hapi"; -import supertokens from "supertokens-node"; -import { plugin } from "supertokens-node/framework/hapi"; - -let server = Hapi.server({ - port: 8000, - routes: { - // highlight-start - cors: { - origin: ["^{appInfo.websiteDomain}"], - additionalHeaders: [...supertokens.getAllCORSHeaders()], - credentials: true, - }, - // highlight-end - }, -}); - -(async () => { - // highlight-next-line - await server.register(plugin); - - await server.start(); -})(); - -// ...your API routes -``` - - - - - - -Register the `plugin`. Also register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody) plugin. - - - -```tsx -import cors from "@fastify/cors"; -import supertokens from "supertokens-node"; -import { plugin } from "supertokens-node/framework/fastify"; -import formDataPlugin from "@fastify/formbody"; - -import fastifyImport from "fastify"; - -let fastify = fastifyImport(); - -// ...other middlewares -// highlight-start -fastify.register(cors, { - origin: "^{appInfo.websiteDomain}", - allowedHeaders: ["Content-Type", ...supertokens.getAllCORSHeaders()], - credentials: true, -}); -// highlight-end - -(async () => { - // highlight-next-line - await fastify.register(formDataPlugin); - // highlight-next-line - await fastify.register(plugin); - - await fastify.listen({ port: 8000 }); -})(); - -// ...your API routes -``` - - - - - - -:::important -Add the `middleware` BEFORE all your routes. -::: - - - -```tsx -import Koa from "koa"; -import cors from "@koa/cors"; -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/koa"; - -let app = new Koa(); - -app.use( - cors({ - // highlight-start - origin: "^{appInfo.websiteDomain}", - allowHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - credentials: true, - // highlight-end - }), -); - -// highlight-next-line -app.use(middleware()); - -// ...your API routes -``` - - - - - - -:::important -Add the `middleware` BEFORE all your routes. -::: - - - -```tsx -import { RestApplication } from "@loopback/rest"; -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/loopback"; - -let app = new RestApplication({ - rest: { - cors: { - // highlight-start - origin: "^{appInfo.websiteDomain}", - allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - credentials: true, - // highlight-end - }, - }, -}); - -// highlight-next-line -app.middleware(middleware); - -// ...your API routes -``` - - - - - - - - - - - - - - -Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. - - - -```go -import ( - "net/http" - "strings" - - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - // SuperTokens init... - - //highlight-start - http.ListenAndServe("SERVER ADDRESS", corsMiddleware( - supertokens.Middleware(http.HandlerFunc(func(rw http.ResponseWriter, - //highlight-end - r *http.Request) { - // TODO: Handle your APIs.. - - })))) -} - -func corsMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(response http.ResponseWriter, r *http.Request) { - response.Header().Set("Access-Control-Allow-Origin", "^{appInfo.websiteDomain}") - response.Header().Set("Access-Control-Allow-Credentials", "true") - if r.Method == "OPTIONS" { - // we add content-type + other headers used by SuperTokens - response.Header().Set("Access-Control-Allow-Headers", - strings.Join(append([]string{"Content-Type"}, - //highlight-start - supertokens.GetAllCORSHeaders()...), ",")) - //highlight-end - response.Header().Set("Access-Control-Allow-Methods", "*") - response.Write([]byte("")) - } else { - next.ServeHTTP(response, r) - } - }) -} -``` - - - - - - - -Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. - - -```go -import ( - "net/http" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - // SuperTokens init... - - router := gin.New() - - // CORS - router.Use(cors.New(cors.Config{ - AllowOrigins: []string{"^{appInfo.websiteDomain}"}, - AllowMethods: []string{"GET", "POST", "DELETE", "PUT", "OPTIONS"}, - AllowHeaders: append([]string{"content-type"}, - // highlight-next-line - supertokens.GetAllCORSHeaders()...), - AllowCredentials: true, - })) - - // Adding the SuperTokens middleware - // highlight-start - router.Use(func(c *gin.Context) { - supertokens.Middleware(http.HandlerFunc( - func(rw http.ResponseWriter, r *http.Request) { - c.Next() - })).ServeHTTP(c.Writer, c.Request) - // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly - c.Abort() - }) - // highlight-end - - // Add APIs and start server -} -``` - - - - - - - -Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. - - - -```go -import ( - "github.com/go-chi/chi" - "github.com/go-chi/cors" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - // SuperTokens init... - - r := chi.NewRouter() - - // CORS - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"^{appInfo.websiteDomain}"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: append([]string{"Content-Type"}, - //highlight-next-line - supertokens.GetAllCORSHeaders()...), - AllowCredentials: true, - })) - - // SuperTokens Middleware - //highlight-next-line - r.Use(supertokens.Middleware) - - // Add APIs and start server -} -``` - - - - - - - -Use the `supertokens.Middleware` and the `supertokens.GetAllCORSHeaders()` functions as shown below. - - - -```go -import ( - "net/http" - - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - // SuperTokens init... - - // TODO: Add APIs - - router := mux.NewRouter() - - // Adding handlers.CORS(options)(supertokens.Middleware(router))) - //highlight-start - http.ListenAndServe("SERVER ADDRESS", handlers.CORS( - handlers.AllowedHeaders(append([]string{"Content-Type"}, - supertokens.GetAllCORSHeaders()...)), - handlers.AllowedMethods([]string{"GET", "POST", "PUT", "HEAD", "OPTIONS"}), - handlers.AllowedOrigins([]string{"^{appInfo.websiteDomain}"}), - handlers.AllowCredentials(), - )(supertokens.Middleware(router))) - //highlight-end -} -``` - - - - - - - - - - - - - -Use the `Middleware` (**BEFORE all your routes**) and the `get_all_cors_headers()` functions as shown below. - - - -```python -from fastapi import FastAPI -from starlette.middleware.cors import CORSMiddleware - -from supertokens_python import get_all_cors_headers -from supertokens_python.framework.fastapi import get_middleware - -app = FastAPI() -# highlight-next-line -app.add_middleware(get_middleware()) - -# TODO: Add APIs - -app.add_middleware( - CORSMiddleware, - allow_origins=[ - "^{appInfo.websiteDomain}" - ], - allow_credentials=True, - allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], - # highlight-next-line - allow_headers=["Content-Type"] + get_all_cors_headers(), -) - -# TODO: start server -``` - - - - - - -- Use the `Middleware` (**BEFORE all your routes and after calling init function**) and the `get_all_cors_headers()` functions as shown below. -- Add a route to catch all paths and return a 404. This is needed because if we don't add this, then OPTIONS request for the APIs exposed by the `Middleware` will return a `404`. - - -```python -from supertokens_python import get_all_cors_headers -from flask import Flask, abort -from flask_cors import CORS # type: ignore -from supertokens_python.framework.flask import Middleware - -app = Flask(__name__) -# highlight-next-line -Middleware(app) - -# TODO: Add APIs - -CORS( - app=app, - origins=[ - "^{appInfo.websiteDomain}" - ], - supports_credentials=True, - # highlight-next-line - allow_headers=["Content-Type"] + get_all_cors_headers(), -) - -# This is required since if this is not there, then OPTIONS requests for -# the APIs exposed by the supertokens' Middleware will return a 404 -# highlight-start -@app.route('/', defaults={'u_path': ''}) # type: ignore -@app.route('/') # type: ignore -def catch_all(u_path: str): - abort(404) -# highlight-end - -# TODO: start server -``` - - - - - - -Use the `Middleware` and the `get_all_cors_headers()` functions as shown below in your `settings.py`. - - - -```python -from typing import List - -from corsheaders.defaults import default_headers - -from supertokens_python import get_all_cors_headers - -CORS_ORIGIN_WHITELIST = [ - "^{appInfo.websiteDomain}" -] - -CORS_ALLOW_CREDENTIALS = True - -CORS_ALLOWED_ORIGINS = [ - "^{appInfo.websiteDomain}" -] - -CORS_ALLOW_HEADERS: List[str] = list(default_headers) + [ - "Content-Type" - # highlight-next-line -] + get_all_cors_headers() - -INSTALLED_APPS = [ - 'corsheaders', - 'supertokens_python' -] - -MIDDLEWARE = [ # type: ignore - 'corsheaders.middleware.CorsMiddleware', - ..., - # highlight-next-line - 'supertokens_python.framework.django.django_middleware.middleware', -] -# TODO: start server -``` - - - - - - - - -You can review all the endpoints that are added through the use of **SuperTokens** by visiting the [API Specs](https://app.swaggerhub.com/apis/supertokens/FDI). - -## 4. Add the SuperTokens Error Handler - -Depending on the language and framework that you are using, you might need to add a custom error handler to your server. -The handler will catch all the authentication related errors and return proper HTTP responses that can be parsed by the frontend SDKs. - - - - - - - - - -```tsx -import express, { Request, Response, NextFunction } from "express"; -import { errorHandler } from "supertokens-node/framework/express"; - -let app = express(); - -// ...your API routes - -// highlight-start -// Add this AFTER all your routes -app.use(errorHandler()); -// highlight-end - -// your own error handler -app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { - /* ... */ -}); -``` - - - - - - -No additional `errorHandler` is required. - - - - - - - - - -Add the `errorHandler` **Before all your routes and plugin registration** - - - -```tsx -import Fastify from "fastify"; -import { errorHandler } from "supertokens-node/framework/fastify"; - -let fastify = Fastify(); - -// highlight-next-line -fastify.setErrorHandler(errorHandler()); - -// ...your API routes -``` - - - - - - - -No additional `errorHandler` is required. - - - - - - - - - -No additional `errorHandler` is required. - - - - - - - - - - - -:::info -You can skip this step -::: - - - - -:::info -You can skip this step -::: - - - - -## 5. Secure Application Routes - -Now that your server can authenticate users, the final step that you need to take care of is to prevent unauthorized access to certain parts of the application. - - - - - -For your APIs that require a user to be logged in, use the `verifySession` middleware. - - - - - -```tsx -import express from "express"; -import { verifySession } from "supertokens-node/recipe/session/framework/express"; -import { SessionRequest } from "supertokens-node/framework/express"; - -let app = express(); - -// highlight-start -app.post("/like-comment", verifySession(), (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - // highlight-end - //.... -}); -``` - - - - -```tsx -import Hapi from "@hapi/hapi"; -import { verifySession } from "supertokens-node/recipe/session/framework/hapi"; -import { SessionRequest } from "supertokens-node/framework/hapi"; - -let server = Hapi.server({ port: 8000 }); - -server.route({ - path: "/like-comment", - method: "post", - //highlight-start - options: { - pre: [ - { - method: verifySession(), - }, - ], - }, - handler: async (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - //highlight-end - //... - }, -}); -``` - - - - -```tsx -import Fastify from "fastify"; -import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; -import { SessionRequest } from "supertokens-node/framework/fastify"; - -let fastify = Fastify(); - -//highlight-start -fastify.post( - "/like-comment", - { - preHandler: verifySession(), - }, - (req: SessionRequest, res) => { - let userId = req.session!.getUserId(); - //highlight-end - //.... - }, -); -``` - - - - -```tsx -import KoaRouter from "koa-router"; -import { verifySession } from "supertokens-node/recipe/session/framework/koa"; -import { SessionContext } from "supertokens-node/framework/koa"; - -let router = new KoaRouter(); - -//highlight-start -router.post("/like-comment", verifySession(), (ctx: SessionContext, next) => { - let userId = ctx.session!.getUserId(); - //highlight-end - //.... -}); -``` - - - - -```tsx -import { inject, intercept } from "@loopback/core"; -import { - RestBindings, - MiddlewareContext, - post, - response, -} from "@loopback/rest"; -import { verifySession } from "supertokens-node/recipe/session/framework/loopback"; -import { SessionContext } from "supertokens-node/framework/loopback"; - -class LikeComment { - //highlight-start - constructor( - @inject(RestBindings.Http.CONTEXT) private ctx: MiddlewareContext, - ) {} - @post("/like-comment") - @intercept(verifySession()) - @response(200) - handler() { - let userId = (this.ctx as SessionContext).session!.getUserId(); - //highlight-end - //.... - } -} -``` - - - - - - - -For your APIs that require a user to be logged in, use the `VerifySession` middleware. - - - - -```go -import ( - "fmt" - "net/http" - - "github.com/supertokens/supertokens-golang/recipe/session" -) - -func main() { - _ = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - // Wrap the API handler in session.VerifySession - session.VerifySession(nil, likeCommentAPI).ServeHTTP(rw, r) - }) -} - -func likeCommentAPI(w http.ResponseWriter, r *http.Request) { - // retrieve the session object as shown below - sessionContainer := session.GetSessionFromRequestContext(r.Context()) - - userID := sessionContainer.GetUserID() - - fmt.Println(userID) -} -``` - - - - -```go -import ( - "fmt" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" -) - -func main() { - router := gin.New() - - // Wrap the API handler in session.VerifySession - router.POST("/likecomment", verifySession(nil), likeCommentAPI) -} - -// This is a function that wraps the supertokens verification function -// to work the gin -func verifySession(options *sessmodels.VerifySessionOptions) gin.HandlerFunc { - return func(c *gin.Context) { - session.VerifySession(options, func(rw http.ResponseWriter, r *http.Request) { - c.Request = c.Request.WithContext(r.Context()) - c.Next() - })(c.Writer, c.Request) - // we call Abort so that the next handler in the chain is not called, unless we call Next explicitly - c.Abort() - } -} - -func likeCommentAPI(c *gin.Context) { - // retrieve the session object as shown below - sessionContainer := session.GetSessionFromRequestContext(c.Request.Context()) - - userID := sessionContainer.GetUserID() - - fmt.Println(userID) -} -``` - - - - -```go -import ( - "fmt" - "net/http" - - "github.com/go-chi/chi" - "github.com/supertokens/supertokens-golang/recipe/session" -) - -func main() { - r := chi.NewRouter() - - // Wrap the API handler in session.VerifySession - r.Post("/likecomment", session.VerifySession(nil, likeCommentAPI)) -} - -func likeCommentAPI(w http.ResponseWriter, r *http.Request) { - // retrieve the session object as shown below - sessionContainer := session.GetSessionFromRequestContext(r.Context()) - - userID := sessionContainer.GetUserID() - - fmt.Println(userID) -} -``` - - - - -```go -import ( - "fmt" - "net/http" - - "github.com/gorilla/mux" - "github.com/supertokens/supertokens-golang/recipe/session" -) - -func main() { - router := mux.NewRouter() - - // Wrap the API handler in session.VerifySession - router.HandleFunc("/likecomment", session.VerifySession(nil, likeCommentAPI)).Methods(http.MethodPost) -} - -func likeCommentAPI(w http.ResponseWriter, r *http.Request) { - // retrieve the session object as shown below - sessionContainer := session.GetSessionFromRequestContext(r.Context()) - - userID := sessionContainer.GetUserID() - - fmt.Println(userID) -} -``` - - - - - - - -For your APIs that require a user to be logged in, use the `verify_session` middleware. - - - - -```python -from fastapi import Depends - -from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.session.framework.fastapi import verify_session - - -# highlight-start -@app.post('/like_comment') # type: ignore -async def like_comment(session: SessionContainer = Depends(verify_session())): - user_id = session.get_user_id() - # highlight-end - - print(user_id) -``` - - - - -```python -from flask import g - -from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.session.framework.flask import verify_session - - -# highlight-start -@app.route('/update-jwt', methods=['POST']) # type: ignore -@verify_session() -def like_comment(): - session: SessionContainer = g.supertokens # type: ignore - - user_id = session.get_user_id() - # highlight-end - - print(user_id) -``` - - - - -```python -from typing import cast - -from django.http import HttpRequest - -from supertokens_python.recipe.session import SessionContainer -from supertokens_python.recipe.session.framework.django.asyncio import verify_session - - -# highlight-start -@verify_session() -async def like_comment(request: HttpRequest): - session: SessionContainer = cast(SessionContainer, request.supertokens) # type: ignore - - user_id = session.get_user_id() - # highlight-end - - print(user_id) -``` - - - - - - - - -The middleware function returns a `401` to the frontend if a session doesn't exist, or if the access token has expired, in which case, our frontend SDK automatically refreshes the session. - -In case of successful session verification, you get access to a `session` object using which you can get the user's ID, or manipulate the session information. - -## 6. Test the Login Flow - -Now that you have configured both the frontend and the backend, you can return to the frontend login page. -From here follow these steps to confirm that your setup is working properly. - -- Click on the **Sign up** button to create a new account. -- After you have created the account go to **Login** page and fill in your credentials. -- If you are greeted with the login screen you have completed the quickstart setup. - -:::success 🎉 Congratulations 🎉 - -You've successfully integrated **SuperTokens** with your existing application! - -Of course, there are additional things that you should add in order to provide a complete authentication experience. -We will talk about those things in the [next section](./next-steps). - -::: diff --git a/docs/quickstart/build-with-ai-tools.mdx b/docs/quickstart/build-with-ai-tools.mdx deleted file mode 100644 index 9c6d0da188..0000000000 --- a/docs/quickstart/build-with-ai-tools.mdx +++ /dev/null @@ -1,304 +0,0 @@ ---- -title: Build with AI Tools -hide_title: true -pagination_next: null -sidebar_position: 6 -description: >- - Learn how to leverage the plain text documentation format inside AI tools. -page_type: tutorial -category: quickstart -skip_llms: true ---- - -# Build with AI Tools - -## Overview - -If you plan on using large language models (LLMs) to assist in the process of integrating SuperTokens, the documentation exposes a set of helpers that can aid you. - -## Text documentation - -You can access all the documentation as plain text markdown files by appending `.md` to the end of any URL or by using the `Copy Markdown` button, from the top right part of each page. -For example, you can find the plain text version of this page at [https://supertokens.com/docs/quickstart/build-with-ai-tools.md](https://supertokens.com/docs/quickstart/build-with-ai-tools.md). - -This plain text format is ideal for AI tools and agents as it: -- Contains fewer formatting tokens. -- Displays content that might otherwise be hidden in the HTML or JavaScript-rendered version (such as content in tabs). -- Maintains a clear markdown hierarchy that LLMs can easily parse and understand. -- Can be read in a paginated format using the `offset` and `length` query parameters. - - -### `/llms.txt` - -The documentation website hosts an [/llms.txt file](https://supertokens.com/docs/llms.txt) which instructs AI tools and agents on how to retrieve the plain text versions of our pages. -The file follows an [emerging standard](https://llmstxt.org/) for enhancing content accessibility to LLMs. - -Additionally, to access the entire documentation content in a single page you can use the [/llms-full.txt file](https://supertokens.com/docs/llms-full.txt). -Keep in mind that this file is really large and might get ignored by LLMs. -You can use the `offset` and `length` query parameters to paginate the content. - -## Model Context Protocol (MCP) Server - -If you want to leverage the agentic capabilities of an LLM tool you can use the **SuperTokens Model Context Protocol** (MCP) server. -The server exposes a set of tools which instruct the LLM on how to access and read the documentation as well as how to administer your authentication integration. - - -### Installation - -You can use the MCP server in two ways: -- over HTTP, as an endpoint exposed by your current server -- as a CLI script, over STDIO - -#### Using HTTP - - - - -### Install the plugin - - - - ```bash - npm i -s supertokens-mcp-plugin - ``` - - - ```bash - yarn add supertokens-mcp-plugin - ``` - - - ```bash - pnpm add supertokens-auth-react supertokens-web-js - ``` - - - -### Update the SDK initialization code - -Initialize and include the `SuperTokensAdminMcpServer` in your SDK configuration. -You have to specify how the client will be authorized through either [claim validators](/docs/additional-verification/session-verification/claim-validation) or through custom logic defined in the `validateTokenPayload` function. - -```ts -import UserRoles, { UserRoleClaim } from "supertokens-node/recipe/userroles"; -import OAuth2Provider from "supertokens-node/recipe/oauth2provider"; -import OpenID from "supertokens-node/recipe/openid"; -import SuperTokensMcpPlugin, { - SuperTokensAdminMcpServer, -} from "supertokens-mcp-plugin"; - -const adminMcpServer = new SuperTokensAdminMcpServer({ - path: "/mcp/admin", - validateTokenPayload: async (accessTokenPayload) => { - // Use custom logic to authenticate who can access the admin MCP server - return { status: "OK" }; - }, - claimValidators: [UserRoleClaim.validators.includes("admin")], -}); - -export const SuperTokensConfig = { - supertokens: { - connectionURI: "", - apiKey: "", - }, - appInfo: { - appName: "", - apiDomain: "", - websiteDomain: "", - apiBasePath: "", - websiteBasePath: "", - }, - recipeList: [ - // Include your existing recipes here - // The OAuth2Provider recipe is required for the MCP authorization process - OAuth2Provider.init(), - ], - // Pass the MCP server through the plguin configuration section - experimental: { - plugins: [ - SuperTokensMcpPlugin.init({ - mcpServers: [adminMcpServer], - }), - ], - }, -}; -``` - -### Add the MCP server in a client configuration - - - -Add the following to your `~/.cursor/mcp.json` file. -To learn more, see the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol). - -```json -{ - "mcpServers": { - "server-with-authentication": { - "command": "npx", - "args": ["mcp-remote", "/mcp"] - } - } -} -``` - - - -Add the following to your `.vscode/mcp.json` file. -To learn more, see the [VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). - -```json -{ - "servers": { - "server-with-authentication": { - "command": "npx", - "args": ["mcp-remote", "/mcp"] - } - } -} -``` - - -Add the following to your `~/.codeium/windsurf/mcp_config.json` file. -To learn more, see the [Windsurf documentation](https://docs.windsurf.com/windsurf/cascade/mcp). - -```json -{ - "mcpServers": { - "server-with-authentication": { - "command": "npx", - "args": ["mcp-remote", "/mcp"] - } - } -} -``` - - -Add the following to your `claude_desktop_config.json` file. -To learn more, see the [Claude Desktop documentation](https://modelcontextprotocol.io/quickstart/user). - - ```json -{ - "mcpServers": { - "server-with-authentication": { - "command": "npx", - "args": ["mcp-remote", "/mcp"] - } - } -} - ``` - - - - - - -#### Using STDIO - -If you plan on using the server locally you can run it directly through `npx`. -You have to provide a set of environment variables that match the SDK configuration values. - - - -Add the following to your `~/.cursor/mcp.json` file. -To learn more, see the [Cursor documentation](https://docs.cursor.com/context/model-context-protocol). - -```json -{ - "mcpServers": { - "supertokens": { - "command": "npx", - "args": ["-y", "supertokens-mcp-plugin"], - "env": { - "APP_NAME":"", - "API_DOMAIN":"", - "WEBSITE_DOMAIN":"", - "API_BASE_PATH":"", - "WEBSITE_BASE_PATH":"", - "CONNECTION_URI":"", - "API_KEY":"API_KEY" - } - } - } -} -``` - - - -Add the following to your `.vscode/mcp.json` file. -To learn more, see the [VS Code documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers). - -```json -{ - "servers": { - "supertokens": { - "command": "npx", - "args": ["-y", "supertokens-mcp-plugin"], - "env": { - "APP_NAME":"", - "API_DOMAIN":"", - "WEBSITE_DOMAIN":"", - "API_BASE_PATH":"", - "WEBSITE_BASE_PATH":"", - "CONNECTION_URI":"", - "API_KEY":"API_KEY" - } - } - } -} -``` - - - -Add the following to your `~/.codeium/windsurf/mcp_config.json` file. -To learn more, see the [Windsurf documentation](https://docs.windsurf.com/windsurf/cascade/mcp). - -```json -{ - "mcpServers": { - "supertokens": { - "command": "npx", - "args": ["-y", "supertokens-mcp-plugin"], - "env": { - "APP_NAME":"", - "API_DOMAIN":"", - "WEBSITE_DOMAIN":"", - "API_BASE_PATH":"", - "WEBSITE_BASE_PATH":"", - "CONNECTION_URI":"", - "API_KEY":"API_KEY" - } - } - } -} -``` - - - -Add the following to your `claude_desktop_config.json` file. -To learn more, see the [Claude Desktop documentation](https://modelcontextprotocol.io/quickstart/user). - -```json -{ - "mcpServers": { - "supertokens": { - "command": "npx", - "args": ["-y", "supertokens-mcp-plugin"], - "env": { - "APP_NAME":"", - "API_DOMAIN":"", - "WEBSITE_DOMAIN":"", - "API_BASE_PATH":"", - "WEBSITE_BASE_PATH":"", - "CONNECTION_URI":"", - "API_KEY":"API_KEY" - } - } - } -} -``` - - - - - diff --git a/docs/quickstart/example-applications.mdx b/docs/quickstart/example-applications.mdx deleted file mode 100644 index 7b26c3102a..0000000000 --- a/docs/quickstart/example-applications.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Example Applications -sidebar_position: 5 -description: Generate and run a demo app to explore SuperTokens authentication setup. -page_type: tutorial -category: quickstart ---- - - -# Example App - - -## Overview - -The fastest way to run a quick demo of **SuperTokens** is to use our cli to generate an example application. -You will get a quick understanding of how the authentication experience works and the overall setup of a project. - - - - - - -## Project Structure - -The command will create a new folder based on the application name that you have specified. -Inside it you will find a project configured to support an authentication flow based on the recipe that you have chosen. - - - -## Next Steps - -If you want more explanations on how to integrate **SuperTokens** with your application, please check our [**Quickstart Guide**](/docs/quickstart/introduction). - -Otherwise, you can explore the other sections of the documentation that expand on the features of **SuperTokens** and -offer you in-depth instructions on how to use them. - - - - - Self Host SuperTokens Core - - - Run the SuperTokens in your own infrastructure. - - - - - Migration Guide - Migrate your data from other authentication providers. - - - - Multi Factor Authentication - Set up additional authentication layers in your sign-in process. - - - - User Management Dashboard - Administrate your users from the SuperTokens Dashboard. - - - - User Roles and Permissions - Adjust the authorization settings in your application. - - - diff --git a/docs/quickstart/frontend-setup.mdx b/docs/quickstart/frontend-setup.mdx deleted file mode 100644 index 506b425dab..0000000000 --- a/docs/quickstart/frontend-setup.mdx +++ /dev/null @@ -1,1415 +0,0 @@ ---- -id: frontend-setup -title: Frontend Setup -hide_title: true -sidebar_position: 2 -description: >- - Configure your frontend application for SuperTokens authentication, including - SDK installation and session management. -page_type: tutorial -category: quickstart ---- - - -import ReactRouterCallout from "/docs/_blocks/react-router-callout.mdx"; - -import ReactSDKInit from "./_blocks/react-sdk-init.mdx"; -import ReactRouterV6 from "./_blocks/react-router-v6.mdx"; -import ReactNoRouter from "./_blocks/react-no-router.mdx"; -import AngularAuthComponent from "./_blocks/angular-auth-component.mdx"; -import AngularAppComponent from "./_blocks/angular-auth-component.mdx"; -import AngularRouting from "./_blocks/angular-routing.mdx"; -import VueAuthView from "./_blocks/vue-auth-view.mdx"; -import VueMainFile from "./_blocks/vue-main-file.mdx"; -import VueRouting from "./_blocks/vue-routing.mdx"; -import VanillaJSNpmSDKInit from "./_blocks/vanilla-js-npm-sdk-init.mdx"; -import VanillaJSScriptsSDKInit from "./_blocks/vanilla-js-script-sdk-init.mdx"; -import KotlinSDKInit from "./_blocks/kotlin-sdk-init.mdx"; -import SwiftSDKInit from "./_blocks/swift-sdk-init.mdx"; -import VanillaJSNpmSignUp from "./_blocks/vanilla-js-npm-sign-up.mdx"; -import VanillaJSScriptsSignUp from "./_blocks/vanilla-js-script-sign-up.mdx"; -import VanillaJSNpmCheckEmail from "./_blocks/vanilla-js-npm-check-email.mdx"; -import VanillaJSScriptsCheckEmail from "./_blocks/vanilla-js-script-check-email.mdx"; -import VanillaJSNpmSignIn from "./_blocks/vanilla-js-npm-sign-in.mdx"; -import VanillaJSScriptsSignIn from "./_blocks/vanilla-js-script-sign-in.mdx"; -import CurlSignIn from "./_blocks/curl-sign-in.mdx"; - - -# Frontend Setup - -Start the setup by configuring your frontend application to use **SuperTokens** for authentication. - -This guide uses the **SuperTokens pre-built UI** components. -If you want to create your own interface please check the **Custom UI** tutorial. - - - - - -## 1. Install the SDK - -Run the following command in your terminal to install the package. - - - - - - ```bash - npm i -s supertokens-auth-react - ``` - - - ```bash - yarn add supertokens-auth-react supertokens-web-js - ``` - - - ```bash - pnpm add supertokens-auth-react supertokens-web-js - ``` - - - ```bash - bun add supertokens-auth-react supertokens-web-js - ``` - - - - - - - ```bash - npm i -s supertokens-web-js - ``` - - - ```bash - yarn add supertokens-web-js - ``` - - - ```bash - pnpm add supertokens-web-js - ``` - - - ```bash - bun add supertokens-web-js - ``` - - - - - - - ```bash - npm i -s supertokens-web-js - ``` - - - ```bash - yarn add supertokens-web-js - ``` - - - ```bash - pnpm add supertokens-web-js - ``` - - - ```bash - bun add supertokens-web-js - ``` - - - - - -## 2. Initialize the SDK - - - - - - In your main application file call the `SuperTokens.init` function to initialize the SDK. - The `init` call includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. - After that you have to wrap the application with the `SuperTokensWrapper` component. - This provides authentication context for the rest of the UI tree. - - - - - Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Angular app. - - **Architecture** - - - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Angular app, so that all pages in your app can use it. - - You have to create a `^{appInfo.websiteBasePath}*` route in the Angular app which renders our pre-built UI. which also needs to be initialised, but only on that route. - - **Creating the `^{appInfo.websiteBasePath}` route** - - - Use the Angular CLI to generate a new route - - ```bash - ng generate module auth --route auth --module app.module - ``` - - - Add the following code to your `auth` angular component - - - - - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the emailpassword and session recipe. - - - Initialize the `supertokens-web-js` SDK in your angular app's root component. This provides session management across your entire application. - - - - - Before we initialize the `supertokens-web-js` SDK let's see how we use it in our Vue app - - **Architecture** - - - The `supertokens-web-js` SDK is responsible for session management and providing helper functions to check if a session exists, or validate the access token claims on the frontend (for example, to check for user roles before showing some UI). We initialise this SDK on the root of your Vue app, so that all pages in your app can use it. - - We create a `^{appInfo.websiteBasePath}*` route in the Vue app which renders our pre-built UI which also needs to be initialised, but only on that route. - - **Creating the `^{appInfo.websiteBasePath}` route** - - - Create a new file `AuthView.vue`, this Vue component is used to render the auth component: - - - - - In the `loadScript` function, we provide the SuperTokens config for the UI. We add the emailpassword and session recipe. - - - Initialize the `supertokens-web-js` SDK in your Vue app's `main.ts` file. This provides session management across your entire application. - - - - - -## 3. Configure Routing - - - - In order for the **pre-built UI** to be rendered inside your application, you have to specify which routes show the authentication components. - The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this. - Based on whether you already use this package or not in your project, there are two different ways of configuring the routes. - - - - Call the `getSuperTokensRoutesForReactRouterDom` method from within any `react-router-dom` `Routes` component. - - - - - Add the highlighted code snippet to your root level `render` function. - - - - - - Update your angular router so that all auth related requests load the `auth` component - - - - Update your Vue router so that all auth related requests load the `AuthView` component - - - - -## 4. Handle Session Tokens - -This part is handled automatically by the **Frontend SDK**. -You don't need to do anything. -The step serves more as a way for us to tell you how is this handled under the hood. - -After you call the `init` function, the **SDK** adds interceptors to both `fetch` and `XHR`, XMLHTTPRequest. The latter is used by the `axios` library. -The interceptors save the session tokens that are generated from the authentication flow. -Those tokens are then added to requests initialized by your frontend app which target the backend API. -By default, the tokens are stored through session cookies but you can also switch to [header based authentication](/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication). - -## 5. Secure Application Routes - -In order to prevent unauthorized access to certain parts of your frontend application you can use our utilities. -Follow the code samples below to understand how to do this. - - - - You can wrap your components with the `` react component. This ensures that your component renders only if the user is logged in. If they are not logged in, the user is redirected to the login page. - ```tsx - import React from "react"; - import { BrowserRouter, Routes, Route } from "react-router-dom"; - // highlight-next-line - import { SessionAuth } from "supertokens-auth-react/recipe/session"; - // @ts-ignore - import MyDashboardComponent from "./dashboard"; - - class App extends React.Component { - render() { - return ( - - - - {/*Components that require to be protected by authentication*/} - - - // highlight-end - } - /> - - - ); - } - } - ``` - - - You can use the `doesSessionExist` function to check if a session exists in all your routes. - ```tsx - import Session from "supertokens-web-js/recipe/session"; - - async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } - } - ``` - - - You can use the `doesSessionExist` function to check if a session exists in all your routes. - ```tsx - import Session from "supertokens-web-js/recipe/session"; - - async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } - } - ``` - - - -## 6. View the login UI - - -You can check the login UI by visiting the `^{appInfo.websiteBasePath}` route, in your frontend application. -To review all the components of our pre-built UI please follow [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground). - - - - - - - -## 1. Install the SDK - -Use the following command to install the required package. - - - - - - ```bash - npm i -s supertokens-web-js - ``` - - - - - You need to add all of the following scripts to your app - - - ```html - - - - - ``` - - - - - :::info - If you want to implement a common authentication experience for both web and mobile, please look at our [**Unified Login guide**](/docs/authentication/unified-login/introduction). - ::: - - - ```bash - npm i -s supertokens-react-native -# IMPORTANT: If you already have @react-native-async-storage/async-storage as a dependency, make sure the version is 1.12.1 or higher - npm i -s @react-native-async-storage/async-storage - ``` - - - Add to your `settings.gradle`: - ```bash - dependencyResolutionManagement { - ... - repositories { - ... - maven { url 'https://jitpack.io' } - } - } - ``` - - Add the following to you app level's `build.gradle`: - ```bash - implementation 'com.github.supertokens:supertokens-android:X.Y.Z' - ``` - - You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-android/releases) (ignore the `v` prefix in the releases). - - - -#### Using Cocoapods - - Add the Cocoapod dependency to your Podfile - - ```bash - pod 'SuperTokensIOS' - ``` - -#### Using Swift Package Manager - - Follow the [official documentation](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app) to learn how to use Swift Package Manager to add dependencies to your project. - - When adding the dependency use the `master` branch after you enter the supertokens-ios repository URL: - - ```bash - https://github.com/supertokens/supertokens-ios - ``` - - - - - -Add the dependency to your pubspec.yaml - -```bash -supertokens_flutter: ^X.Y.Z -``` - -You can find the latest version of the SDK [here](https://github.com/supertokens/supertokens-flutter/releases) (ignore the `v` prefix in the releases). - - - - - - - - - -## 2. Initialise SuperTokens - - - -Call the SDK init function at the start of your application. -The invocation includes the [main configuration details](/docs/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup. - - - - - - - - - - - - - - - - - ```tsx - import SuperTokens from 'supertokens-react-native'; - - SuperTokens.init({ - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - }); - ``` - - - Add the `SuperTokens.init` function call at the start of your application. - - - - - - - ```dart - import 'package:supertokens_flutter/supertokens.dart'; - - void main() { - SuperTokens.init( - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - ); - } - ``` - - - - - - -## 3. Add the Login UI - -The **Email/Password** flow involves two types of user interfaces. -One for registering and creating new users, the *Sign Up Form*. -And one for the actual authentication attempt, the *Sign In Form*. -If you are provisioning users from a different method you can skip over adding the sign up form. - -### 3.1 Add the Sign Up form - - - - For the **Sign Up** flow you have to first add the UI elements which render your form. - After that, call the following function when the user submits the form that you have previously created. - - - -```tsx -import { signUp } from "supertokens-web-js/recipe/emailpassword"; - -async function signUpClicked(email: string, password: string) { - try { - let response = await signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - // one of the input formFields failed validation - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax), - // or the email is not unique. - window.alert(formField.error) - } else if (formField.id === "password") { - // Password validation failed. - // Maybe it didn't match the password strength - window.alert(formField.error) - } - }) - } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign up was not allowed. - window.alert(response.reason) - } else { - // sign up successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; -async function signUpClicked(email: string, password: string) { - try { - let response = await supertokensEmailPassword.signUp({ - formFields: [{ - id: "email", - value: email - }, { - id: "password", - value: password - }] - }) - - if (response.status === "FIELD_ERROR") { - // one of the input formFields failed validation - response.formFields.forEach(formField => { - if (formField.id === "email") { - // Email validation failed (for example incorrect email syntax), - // or the email is not unique. - window.alert(formField.error) - } else if (formField.id === "password") { - // Password validation failed. - // Maybe it didn't match the password strength - window.alert(formField.error) - } - }) - } else if (response.status === "SIGN_UP_NOT_ALLOWED") { - // the reason string is a user friendly message - // about what went wrong. It can also contain a support code which users - // can tell you so you know why their sign in was not allowed. - window.alert(response.reason) - } else { - // sign up successful. The session tokens are automatically handled by - // the frontend SDK. - window.location.href = "/homepage" - } - } catch (err: any) { - if (err.isSuperTokensGeneralError === true) { - // this may be a custom error message sent from the API by you. - window.alert(err.message); - } else { - window.alert("Oops! Something went wrong."); - } - } -} -``` - - - - - - For the **Sign Up** flow you have to first add the UI elements which render your form. - After that, call the following API when the user submits the form that you have previously created. - - ```bash - curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/signup' \ - --header 'Content-Type: application/json; charset=utf-8' \ - --data-raw '{ - "formFields": [{ - "id": "email", - "value": "john@example.com" - }, { - "id": "password", - "value": "somePassword123" - }] - }' - ``` - - The response body from the API call has a `status` property in it: - - `status: "OK"`: User creation was successful. The response also contains more information about the user, for example their user ID. - - `status: "FIELD_ERROR"`: One of the form field inputs failed validation. The response body contains information about which form field input based on the `id`: - - The email could fail validation if it's syntactically not an email, of it it's not unique. - - The password could fail validation if it's not string enough (as defined by the backend password validator). - - Either way, you want to show the user an error next to the input form field. - - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - - `status: "SIGN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign up was not allowed. - - - - -The `formFields` input is a key-value array. You must provide it an `email` and a `password` value at a minimum. If you want to provide additional items, for example the user's name or age, you can append it to the array like so: - -```json -{ - "formFields": [{ - "id": "email", - "value": "john@example.com" - }, { - "id": "password", - "value": "somePassword123" - }, { - "id": "name", - "value": "John Doe" - }] -} -``` - -On the backend, the `formFields` array is available to you for consumption. - -On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. - -#### How to check if an email is unique - -As a part of the sign up form, you may want to explicitly check that the entered email is unique. -Whilst this is already done via the sign up API call, it may be a better UX to warn the user about a non unique email right after they finish typing it. - - - - - - - - - - - - - - -```bash -curl --location --request GET '^{appInfo.apiDomain}^{appInfo.apiBasePath}/emailpassword/email/exists?email=john@example.com' -``` - -The response body from the API call has a `status` property in it: -- `status: "OK"`: The response also contains a `exists` boolean which is `true` if the input email already belongs to an email password user. -- `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - - - - -### 3.2 Add the Sign In Form - - - - For the **Sign In** flow you have to first add the UI elements which render your form. - After that, call the following function when the user submits the form that you have previously created. - - - - - - - - - - - For the **Sign In** flow you have to first add the UI elements which render your form. - After that, call the following API when the user submits the form that you have previously created. - - - The response body from the API call has a `status` property in it: - - `status: "OK"`: User sign in was successful. The response also contains more information about the user, for example their user ID. - - `status: "WRONG_CREDENTIALS_ERROR"`: The input email and password combination is incorrect. - - `status: "FIELD_ERROR"`: This indicates that the input email did not pass the backend validation - probably because it's syntactically not an email. You want to show the user an error next to the email input form field. - - `status: "GENERAL_ERROR"`: This is only possible if you have overridden the backend API to send back a custom error message which should be displayed on the frontend. - - `status: "SIGN_IN_NOT_ALLOWED"`: This can happen during automatic account linking or during MFA. The `reason` prop that's in the response body contains a support code using which you can see why the sign in was not allowed. - - - - -On success, the backend sends back session tokens as part of the response headers which are automatically handled by our frontend SDK for you. - -## 4. Handle Session Tokens - -You can use sessions with SuperTokens in two modes: -- Using `httpOnly` cookies -- Authorization bearer token. - -Our frontend SDK uses `httpOnly` cookie based session for websites by default as it secures against tokens theft via XSS attacks. -For other platforms, like mobile apps, we use a bearer token in the `Authorization` header by default. - -### With the Frontend SDK - - - - -:::success -No action required. -::: - -Our frontend SDK handles everything for you. You only need to make sure that you have called `supertokens.init` before making any network requests. - -Our SDK adds interceptors to `fetch` and `XHR` (used by `axios`) to save and add session tokens from and to the request. - -By default, our web SDKs use cookies to provide credentials. - - - - - - - - - - - Our frontend SDK handles everything for you. You only need to make sure that you have added our network interceptors as shown below - - - -:::note -By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. -::: - - - - - - - { - return ( - Are you using axios.create? - ) - }}> - - -```tsx -import axios from "axios"; -// highlight-next-line -import SuperTokens from 'supertokens-react-native'; - -let axiosInstance = axios.create({/*...*/}); -// highlight-next-line -SuperTokens.addAxiosInterceptors(axiosInstance); - -async function callAPI() { - // use axios as you normally do - let response = await axiosInstance.get("https://yourapi.com"); -} -``` - - - - - -:::important -You must call `addAxiosInterceptors` on all `axios` imports. -::: - -```tsx -import axios from "axios"; -// highlight-start -import SuperTokens from 'supertokens-react-native'; -SuperTokens.addAxiosInterceptors(axios); -// highlight-end - -async function callAPI() { - // use axios as you normally do - let response = await axios.get("https://yourapi.com"); -} -``` - - - - - - - - - - - - -:::success -When using `fetch`, network interceptors are added automatically when you call `supertokens.init`. So no action needed here. -::: - - - - - - - - - - - - - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens -import com.supertokens.session.SuperTokensHttpURLConnection -import com.supertokens.session.SuperTokensPersistentCookieStore -import java.net.URL -import java.net.HttpURLConnection - -class MainApplication: Application() { - override fun onCreate() { - super.onCreate() - // TODO: Make sure to call SuperTokens.init - } - - fun makeRequest() { - val url = URL("") - val connection = SuperTokensHttpURLConnection.newRequest(url, object: SuperTokensHttpURLConnection.PreConnectCallback { - override fun doAction(con: HttpURLConnection?) { - // TODO: Use `con` to set request method, headers etc - } - }) - - // Handle response using connection object, for example: - if (connection.responseCode == 200) { - // TODO: implement - } - } -} -``` - -:::note -When making network requests you do not need to call `HttpURLConnection.connect` because SuperTokens does this for you. -::: - - - - -```kotlin -import android.content.Context -import com.supertokens.session.SuperTokens -import com.supertokens.session.SuperTokensInterceptor -import okhttp3.OkHttpClient -import retrofit2.Retrofit - -class NetworkManager { - fun getClient(context: Context): OkHttpClient { - val clientBuilder = OkHttpClient.Builder() - clientBuilder.addInterceptor(SuperTokensInterceptor()) - // TODO: Make sure to call SuperTokens.init - - val client = clientBuilder.build() - - // REQUIRED FOR RETROFIT ONLY - val instance = Retrofit.Builder() - .baseUrl("") - .client(client) - .build() - - return client - } - - fun makeRequest(context: Context) { - val client = getClient(context) - // Use client to make requests normally - } -} -``` - - - - -:::note -By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. -::: - - - - - - - - - -

Using URLSession.shared

- -```swift -import Foundation -import SuperTokensIOS - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - URLProtocol.registerClass(SuperTokensURLProtocol.self) - } -} -``` - -

Using a custom URLSession instance

- -```swift -import Foundation -import SuperTokensIOS - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - let configuration = URLSessionConfiguration.default - configuration.protocolClasses = [SuperTokensURLProtocol.self] - let session = URLSession(configuration: configuration) - - // Use session when making network requests - } -} -``` - -
- - - -```swift -import Foundation -import SuperTokensIOS -import Alamofire - -fileprivate class NetworkManager { - func setupSuperTokensInterceptor() { - let configuration = URLSessionConfiguration.af.default - configuration.protocolClasses = [SuperTokensURLProtocol.self] + (configuration.protocolClasses ?? []) - let session = Session(configuration: configuration) - - // Use session when making network requests - } -} -``` - - - -
- -:::note -By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. -::: - -
- - - - - - - -You can make requests as you normally would with `http`, the only difference is that you import the client from the SuperTokens package instead. - -```dart -// Import http from the SuperTokens package -import 'package:supertokens_flutter/http.dart' as http; - -Future makeRequest() async { - Uri uri = Uri.parse("http://localhost:3001/api"); - var response = await http.get(uri); - // handle response -} -``` - -

Using a custom HTTP client

- -If you use a custom HTTP client and want to use SuperTokens, you can simply provide the SDK with your client. All requests continue to use your client along with the session logic that SuperTokens provides. - -```dart -// Import http from the SuperTokens package -import 'package:supertokens_flutter/http.dart' as http; - -Future makeRequest() async { - Uri uri = Uri.parse("http://localhost:3001/api"); - - // Initialise your custom client - var customClient = http.Client(); - // provide your custom client to SuperTokens - var httpClient = http.Client(client: customClient); - - var response = await httpClient.get(uri); - // handle response -} -``` - -
- - -

Add the SuperTokens interceptor

- -Use the extension method provided by the SuperTokens SDK to enable interception on your `Dio` client. This allows the SuperTokens SDK to handle session tokens for you. - -```dart -import 'package:supertokens_flutter/dio.dart'; -import 'package:dio/dio.dart'; - -void setup() { - Dio dio = Dio(); // Create a Dio instance. - dio.addSupertokensInterceptor(); -} -``` - -

Making network requests

- -You can make requests as you normally would with `dio`. - -```dart -import 'package:supertokens_flutter/dio.dart'; -import 'package:dio/dio.dart'; - -void setup() { - Dio dio = Dio( - // Provide your config here - ); - dio.addSupertokensInterceptor(); - - var response = dio.get("http://localhost:3001/api"); - // handle response -} -``` - -
-
- -:::note -By default our mobile SDKs use a bearer token in the Authorization header to provide credentials. -::: - -
- -
- -
-
- - -### Without the Frontend SDK - -:::caution -We highly recommend using our frontend SDK to handle session token management. It saves you a lot of time. -::: - -In this case, you need to manually handle the tokens and session refreshing, and decide if you are going to use header or cookie-based sessions. - -For browsers, we recommend cookies, while for mobile apps (or if you don't want to use the built-in cookie manager) you should use header-based sessions. - - - - - -#### During the Login Action - -You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "cookie". - -The login API returns the following headers: -- `Set-Cookie`: This contains the `sAccessToken`, `sRefreshToken` cookies which are `httpOnly` and are automatically managed by the browser. For mobile apps, you need to setup cookie handling yourself, use our SDK or use a header based authentication mode. -- `front-token` header: This contains information about the access token: - - The userID - - The expiry time of the access token - - The payload added by you in the access token. - - Here is the structure of the token: - ```tsx - let frontTokenFromRequestHeader = "..."; - let frontTokenDecoded = JSON.parse(decodeURIComponent(escape(atob(frontTokenFromRequestHeader)))); - console.log(frontTokenDecoded); - /* - { - ate: 1665226412455, // time in milliseconds for when the access token expires, and then a refresh is required - uid: "....", // user ID - up: { - sub: "..", - iat: .., - ... // other access token payload - } - } - - */ - ``` - - This token is mainly used for cookie based auth because you don't have access to the actual access token on the frontend (for security reasons), but may want to read its payload (for example to know the user's role). This token itself is not signed and hence can't be used in place of the access token itself. You may want to save this token in `localstorage` or in frontend cookies (using `document.cookies`). - -- `anti-csrf` header (optional): By default it's not required, so it's not sent. But if this is sent, you should save this token as well for use when making requests. - -#### When You Make Network Requests to Protected APIs - -The `sAccessToken` gets attached to the request automatically by the browser. Other than that, you need to add the following headers to the request: -- `rid: "anti-csrf"` - this prevents against anti-CSRF requests. If your `apiDomain` and `websiteDomain` values are exactly the same, then this is not necessary. -- `anti-csrf` header (optional): If this was provided to you during login, then you need to add that token as the value of this header. -- You need to set the `credentials` header to `true` or `include`. This is achieved different based on the library you are using to make requests. - -An API call can potentially update the `sAccessToken` and `front-token` tokens, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for: -- `sAccessToken`: This is as a new `Set-Cookie` header and is managed by the browser automatically. -- `front-token`: This should be read and saved by you in the same way as it's being done during login. - -#### Handling session refreshing - -If any of your API calls return with a status code of `401`, it means that the access token has expired. This requires you to refresh the session before retrying the same API call. - -You can call the refresh API as follows: - - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/session/refresh' \ ---header 'Cookie: sRefreshToken=...' -``` - -:::note -- You may also need to add the `anti-csrf` header to the request if that was provided to you during sign in. -- The cURL command above shows the `sRefreshToken` cookie as well, but this is added by the web browser automatically, so you don't need to add it explicitly. -::: - - -The result of a session refresh is either: -- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. -- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `front-token` that you saved on the frontend earlier. - - - - - -##### During the Login Action - -You should attach the `st-auth-mode` header to calls to the login API, but this header is safe to attach to all requests. In this case it should be set to "header". - -The login API returns the following headers: -- `st-access-token`: This contains the current access token associated with the session. You should save this in your application (e.g., in frontend `localstorage`). -- `st-refresh-token`: This contains the current refresh token associated with the session. You should save this in your application (e.g., in frontend `localstorage`). - -#### When You Make Network Requests to Protected APIs - -You need to add the following headers to request: -- `authorization: Bearer {access-token}` -- You need to set the `credentials` to `true` or `include`. This is achieved different based on the library you are using to make requests. - -An API call can potentially update the `access-token`, for example if you call the `mergeIntoAccessTokenPayload` function on the `session` object on the backend. This kind of update is reflected in the response headers for your API calls. The headers contain new values for `st-access-token` - -These should be read and saved by you in the same way as it's being done during login. - -#### Handling session refreshing - -If any of your API calls return with a status code of `401`, it means that the access token has expired. This requires you to refresh the session before retrying the same API call. - -You can call the refresh API as follows: - - -```bash -curl --location --request POST '^{appInfo.apiDomain}^{appInfo.apiBasePath}/session/refresh' \ ---header 'authorization: Bearer {refresh-token}' -``` - - -The result of a session refresh is either: -- Status code `200`: This implies a successful refresh. The set of tokens returned here is the same as when the user logs in, so you can handle them in the same way. -- Status code `401`: This means that the refresh token is invalid, or has been revoked. You must ask the user to login again. Remember to clear the `st-refresh-token` and `st-access-token` that you saved on the frontend earlier. - - - - - - -## 5. Protect Frontend Routes - - - - - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```tsx -import Session from 'supertokens-web-js/recipe/session'; - -async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } -} -``` - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```tsx -import Session from 'supertokens-web-js-script/recipe/session'; -async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } -} -``` - - - - - - - - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```tsx -import SuperTokens from 'supertokens-react-native'; - -async function doesSessionExist() { - if (await SuperTokens.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } -} -``` - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens - -class MainApplication: Application() { - fun doesSessionExist() { - if (SuperTokens.doesSessionExist(this.applicationContext)) { - // user is logged in - } else { - // user has not logged in yet - } - } -} -``` - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```swift -import UIKit -import SuperTokensIOS - -fileprivate class ViewController: UIViewController { - func doesSessionExist() { - if SuperTokens.doesSessionExist() { - // User is logged in - } else { - // User is not logged in - } - } -} -``` - - - - - -You can use the `doesSessionExist` function to check if a session exists. - -```dart -import 'package:supertokens_flutter/supertokens.dart'; - -Future doesSessionExist() async { - return await SuperTokens.doesSessionExist(); -} -``` - - - - - - - - - -## 6. Add a Sign Out Action - -The `signOut` method revokes the session on the frontend and on the backend. Calling this function without a valid session also yields a successful response. - - - - - - ```tsx - import Session from "supertokens-web-js/recipe/session"; - - async function logout () { - // highlight-next-line - await Session.signOut(); - window.location.href = "/auth"; // or to wherever your logic page is - } - ``` - - - ```tsx - import supertokensSession from "supertokens-web-js-script/recipe/session"; - async function logout () { - // highlight-next-line - await supertokensSession.signOut(); - window.location.href = "/auth"; // or to wherever your logic page is - } - ``` - - - - - - - -```tsx -import SuperTokens from "supertokens-react-native"; - -async function logout () { - // highlight-next-line - await SuperTokens.signOut(); - // navigate to the login screen.. -} -``` - - - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens - -class MainApplication: Application() { - fun logout() { - // highlight-next-line - SuperTokens.signOut(this); - // navigate to the login screen.. - } -} -``` - - - -```swift -import UIKit -import SuperTokensIOS - -fileprivate class ViewController: UIViewController { - func signOut() { - SuperTokens.signOut(completionHandler: { - error in - - if error != nil { - // handle error - } else { - // Signed out successfully - } - }) - } -} -``` - - -```dart -import 'package:supertokens_flutter/supertokens.dart'; - -Future signOut() async { - await SuperTokens.signOut( - completionHandler: (error) { - // handle error if any - } - ); -} -``` - - - - - -- On success, the `signOut` function does not redirect the user to another page, so you must redirect the user yourself. -- The `signOut` function calls the sign out API exposed by the session recipe on the backend. -- If you call the `signOut` function whilst the access token has expired, but the refresh token still exists, our SDKs do an automatic session refresh before revoking the session. - -
- -:::success 🎉 Congratulations 🎉 - -Congratulations! You've successfully integrated your frontend app with SuperTokens. - -The [next section](./backend-setup) guides you through setting up your backend and then you should be able to complete a login flow. - -::: diff --git a/docs/quickstart/integrations/_category_.json b/docs/quickstart/integrations/_category_.json deleted file mode 100644 index 0563a646eb..0000000000 --- a/docs/quickstart/integrations/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Integrations", - "position": 7 -} diff --git a/docs/quickstart/integrations/aws-lambda/_category_.json b/docs/quickstart/integrations/aws-lambda/_category_.json deleted file mode 100644 index 11aa72d541..0000000000 --- a/docs/quickstart/integrations/aws-lambda/_category_.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "AWS Lambda", - "collapsible": true, - "collapsed": true, - "position": 2 -} diff --git a/docs/quickstart/integrations/aws-lambda/appsync-integration.mdx b/docs/quickstart/integrations/aws-lambda/appsync-integration.mdx deleted file mode 100644 index 900c1f8a8b..0000000000 --- a/docs/quickstart/integrations/aws-lambda/appsync-integration.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -id: appsync-integration -title: AppSync integration -hide_title: true -sidebar_position: 6 -pagination_next: null -llms_txt_title: AWS Lambda - AppSync integration -description: >- - Integrate Supertokens with AppSync using Lambda Authorizer for secure API - Gateway configuration. -page_type: tutorial -category: quickstart ---- - -# AppSync integration - -## Overview - -A Lambda Authorizer configured like in the [Authorizer guide](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) can help integrate SuperTokens with an AppSync service. - -## Before you start - -The following steps assume that you already have configured **SuperTokens** in AWS Lambda. -If not, please refer to the [AWS Lambda integration guide](/docs/quickstart/integrations/aws-lambda/quickstart-guide). - -## Steps - -### 1. Set up the AppSync service - -Set up the AppSync service with an API key authorization. For more details, please see the [AWS documentation](https://docs.aws.amazon.com/appsync/latest/devguide/quickstart.html). - -### 2. Configure the API Gateway with the authorizer - -Follow the [Authorizer guide](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) to set up the API Gateway with the `/auth`, and `/graphql` resources set up. `/auth` should be pointed to a lambda that handles the auth APIs. - -When setting up the POST method on `/graphql`, you should use the following settings: -- Integration type: AWS service -- AWS Region: the region of the AppSync service -- AWS Service: AppSync Data Plane -- AWS Subdomain: the part of the domain of the GraphQL service before `.appsync-api.` -- HTTP method: POST -- Action type: Use path override -- Path override: `/graphql` -- Execution role: the ARN of an execution role that is authorized to call the AppSync service (e.g.: `AWSAppSyncInvokeFullAccess`) - -### 3. Set up the integration headers - -Configure the "Integration Request" of the `/graphql` POST method. - -- Add `HTTP Header` mappings: - - "x-api-key": The API key of the App Sync service, wrapped in single quotes. - - "x-user-id": `context.authorizer.principalId`, without quotes. - -### 4. Consume the context in resolvers - -You can access the headers you mapped above in resolvers through the context. (e.g., $context.request.headers.custom) For more information, please see the [resolver context](https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html) docs. diff --git a/docs/quickstart/integrations/aws-lambda/quickstart-guide.mdx b/docs/quickstart/integrations/aws-lambda/quickstart-guide.mdx deleted file mode 100644 index 99fbd6bbb2..0000000000 --- a/docs/quickstart/integrations/aws-lambda/quickstart-guide.mdx +++ /dev/null @@ -1,411 +0,0 @@ ---- -title: Quickstart Guide -hide_title: true -toc_max_heading_level: 4 -sidebar_position: 1 -llms_txt_title: AWS Lambda - Quickstart Guide -description: Learn how to integrate SuperTokens with AWS Lambda -page_type: tutorial -category: quickstart ---- - - - -# Quickstart Guide - -The following guide shows you how to use **SuperTokens** in an AWS Lambda environment. -You can also check out the [example repository](https://github.com/supertokens/supertokens-node/tree/master/examples/aws/with-emailpassword) for a full implementation. - -## Before you start - -These instructions assume that you have completed the [quickstart guide](/docs/quickstart/frontend-setup). -If not, please go through it and create the example application before you start this tutorial. - -## Steps - -:::caution -Follow the [quickstart guide](/docs/quickstart/frontend-setup) first to learn how to set up the frontend. -::: - -### 1. Set up API Gateway - -#### 1.1 Create a REST API Gateway - -We will be using AWS API Gateway to create a REST API that will be used to communicate with our Lambda functions. - -Create API gateway step UI - -#### 1.2 Set up authentication routes - -Create a `/auth` resource and then `/auth/{proxy+}` resources. -This will act as a catch-all for all SuperTokens auth routes. -**Enable CORS** while creating the proxy resource. - -Create proxy route step UI - -Route creation complete step UI - -#### 1.3 Attach lambda to the `ANY` method of the proxy resource - -Click on the "ANY" method and then "Integration" to configure the lambda function. -Check **Lambda proxy integration** and then select your lambda function. - -Configure lambda integration UI - -:::important - -Ensure that the **Lambda proxy integration** toggle is turned on. - -::: - -#### 1.4 Enable CORS for the proxy path - -Click on the `{proxy+}` resource and then "Enable CORS" button to open the CORS configuration page. - -Enable CORS for the proxy path UI - - -In the CORS configuration page do the following: - - Select the 'Default 4XX' and 'Default 5XX' checkboxes under Gateway responses - - Select the 'OPTIONS' checkbox under Methods - - Add `rid,fdi-version,anti-csrf,st-auth-mode` to the existing `Access-Control-Allow-Headers` - - Set `Access-Control-Allow-Origin` to `'^{appInfo.websiteDomain}'` - - Select `Access-Control-Allow-Credentials` checkbox - -CORS configuration page - -#### 1.5 Deploy the API Gateway - -Click the **Deploy API** button in the top right corner to deploy the API. During deployment, you'll be prompted to create a stage; for this tutorial, name the stage `dev`. After deployment, you will receive your `Invoke URL`. - -:::important -Update `apiDomain`, `apiBasePath`, and `apiGatewayPath` in both Lambda configuration and your frontend config if they have changed post API Gateway configuration. -::: - -### 2. Set up Lambda layer - -#### 2.1 Create Lambda layer with required libraries - - - - - -```bash -mkdir lambda && cd lambda - -npm i -s supertokens-node @middy/core @middy/http-cors -mkdir nodejs && cp -r node_modules nodejs -zip -r supertokens-node.zip nodejs/ -``` - - - - -```bash -mkdir lambda && cd lambda - -pip install --target ./python fastapi uvicorn mangum nest_asyncio supertokens-python -cd python && rm -r *dist-info **/__pycache__ && cd .. -zip -r supertokens-python.zip python/ -``` - - - - -#### 2.2 Upload SuperTokens lambda layer - -Open AWS Lambda dashboard and click on layers: -AWS Lambda sidebar UI - -Click "Create Layer" button: -Create layer button UI - -Give a name for your layer, upload the zip and select the runtime - - - - Lambda layer node configuration UI - - - Lambda layer python configuration UI - - - -### 3. Set up Lambda - -#### 3.1 Create a new lambda - - Click "Create Function" in the AWS Lambda dashboard, enter the function name and runtime, and create your Lambda function. - - - Create new Lambda configurations UI Node - - - Create new Lambda configurations UI Python - - - -#### 3.2 Link lambda layer with the lambda function - -Scroll to the bottom and look for the `Layers` tab. Click on `Add a layer` -Link Lambda function with the Lambda layer - -Select `Custom Layer` and then select the layer we created in the first step: - - - - Link custom layer with Lambda function Node - - - Link custom layer with Lambda function Python - - - -#### 3.3 Create a backend config file - -Using the editor provided by AWS, create a new config file and write the following code: - - - - - - - - -```tsx title="config.mjs" showAppTypeSelect -import EmailPassword from "supertokens-node/recipe/emailpassword"; -import Session from "supertokens-node/recipe/session"; - -export function getBackendConfig() { - return { - framework: "awsLambda", - supertokens: { - connectionURI: "^{coreInfo.uri}", - // apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - apiGatewayPath: "/dev" - }, - recipeList: [ - EmailPassword.init(), - Session.init(), - ], - isInServerlessEnv: true, - } -} -``` - - - - - - -```python title="config.py" showAppTypeSelect -from supertokens_python.recipe import emailpassword, session -from supertokens_python import SupertokensConfig, InputAppInfo - -supertokens_config = SupertokensConfig( - connection_uri="^{coreInfo.uri}", - # api_key="^{coreInfo.key}" -) - -app_info = InputAppInfo( - # learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}", - api_gateway_path="/dev", -) - -framework = "fastapi" - -recipe_list = [ - session.init(), - emailpassword.init(), -] -``` - - - - - - -:::important -In the above code, notice the extra config of `apiGatewayPath` that was added to the `appInfo` object. -The value of this should be whatever you have set as the value of your [AWS stage](https://docs.aws.amazon.com/apigateway/latest/developerguide/stages.html) which scopes your API endpoints. -For example, you may have a stage name per development environment: -- One for development (`/dev`). -- One for testing (`/test`). -- One for prod (`/prod`). - -So the value of `apiGatewayPath` should be set according to the above based on the environment it's running under. - -You also need to change the `apiBasePath` on the frontend config to append the stage to the path. For example, if the frontend is query the development stage and the value of `apiBasePath` is `/auth`, you should change it to `/dev/auth`. -::: - -:::note -You may edit the `apiBasePath` and `apiGatewayPath` value later if you haven't setup the API Gateway yet. -::: - -#### 3.4 Add the SuperTokens auth middleware - -Using the editor provided by AWS, create/replace the handler file contents with the following code: - - - - -```tsx title="index.mjs" -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/awsLambda"; -// @ts-ignore -import { getBackendConfig } from "./config.mjs"; -import middy from "@middy/core"; -import cors from "@middy/http-cors"; - -supertokens.init(getBackendConfig()); - -export const handler = middy( - // @ts-ignore - middleware((event) => { - // SuperTokens middleware didn't handle the route, return your custom response - return { - body: JSON.stringify({ - msg: "Hello!", - }), - statusCode: 200, - }; - }) -) - .use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) - ) - .onError((request) => { - throw request.error; - }); -``` - -Add SuperTokens auth middleware UI -
-:::important -Since, we are using `esm` imports, we will need to set `NODE_OPTIONS="--experimental-specifier-resolution=node"` flag in the lambda environment variables. See the [Node.js](https://nodejs.org/docs/latest-v16.x/api/esm.html#customizing-esm-specifier-resolution-algorithm) documentation for more information. - -Configuring environment variables UI -::: - -
- - - -```python title="handler.py" -import nest_asyncio # type: ignore -nest_asyncio.apply() # type: ignore - -from fastapi import FastAPI -from starlette.middleware.cors import CORSMiddleware -from mangum import Mangum - -from supertokens_python import init, get_all_cors_headers -from supertokens_python.framework.fastapi import get_middleware - -import config # type: ignore - -init( - supertokens_config=config.supertokens_config, # type: ignore - app_info=config.app_info, # type: ignore - framework=config.framework, # type: ignore - recipe_list=config.recipe_list, # type: ignore - mode="asgi", -) - -app = FastAPI(title="SuperTokens Example") - -app.add_middleware(get_middleware()) - -app = CORSMiddleware( - app=app, - allow_origins=[ - config.app_info.website_domain # type: ignore - ], - allow_credentials=True, - allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], - allow_headers=["Content-Type"] + get_all_cors_headers(), -) - -handler = Mangum(app) -``` - - -
- -#### 3.5 Filter additional plugins or extensions {{optional}} - -If you are using AWS Lambda plugins, extensions, or anything that adds events to the lambda function (e.g. `serverless-plugin-warmup`), then you may need to prevent calling SuperTokens with them. - -These kind of events lack request details that SuperTokens expects, thus might lead to un-intended errors. - -Here's an example of how you can filter them out: - - -```tsx title="index.mjs" -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/awsLambda"; -// @ts-ignore -import { getBackendConfig } from "./config.mjs"; -import middy from "@middy/core"; -import cors from "@middy/http-cors"; - -supertokens.init(getBackendConfig()); - -const httpHandler = middy( - // @ts-ignore - middleware((event) => { - // SuperTokens middleware didn't handle the route, return your custom response - return { - body: JSON.stringify({ - msg: "Hello!", - }), - statusCode: 200, - }; - }) -) - .use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) - ) - .onError((request) => { - throw request.error; - }); - -// @ts-expect-error -const postAuth = async (event, context) => { - // Plugins generally inject a `source` property in the event object. - if (event.source === 'serverless-plugin-warmup') { - console.info('postAuth 010: warming up lambda. Bypassing authMiddleware.'); - return { - statusCode: 200, - body: JSON.stringify({ message: 'Warm-up successful' }), - }; - } - - return httpHandler(event, context); -}; - -export const handler = postAuth; -``` diff --git a/docs/quickstart/integrations/aws-lambda/session-verification.mdx b/docs/quickstart/integrations/aws-lambda/session-verification.mdx deleted file mode 100644 index d585c430cc..0000000000 --- a/docs/quickstart/integrations/aws-lambda/session-verification.mdx +++ /dev/null @@ -1,631 +0,0 @@ ---- -title: Session Verification -hide_title: true -sidebar_position: 2 -llms_txt_title: AWS Lambda - Session Verification -description: Verify user sessions when integrating SuperTokens with AWS Lambda. -page_type: tutorial -category: quickstart ---- - - -import OAuthVerifyTokensCallout from "../../../_blocks/oauth-tokens-callout.mdx"; - -# Session verification - -The following page shows you three different ways to verify sessions in a lambda integration. -Choose the one that works best based on the particularities of your use case. - - - - -## Using Session Verification - -When building your own APIs, you may need to verify the session of the user before proceeding further. -SuperTokens SDK exposes a `verifySession` function that can be utilized for this. -In this guide, we will be creating a `/user` `GET` route that will return the current session information. - -### 1. Add `/user` `GET` route in your API Gateway - -Create a `/user` resource and then `GET` method in your API Gateway. Configure the lambda integration and CORS just like we did [for the auth routes](/docs/quickstart/integrations/aws-lambda/quickstart-guide#13-attach-lambda-to-the-any-method-of-the-proxy-resource). - -### 2. Create a file in your lambda to handle the `/user` route. - - - - -An example of this is [here](https://github.com/supertokens/supertokens-node/blob/master/examples/aws/with-emailpassword/backend/user.mjs). - -```tsx title="user.mjs" -import supertokens from "supertokens-node"; -// @ts-ignore -import { getBackendConfig } from "./config.mjs"; -import { SessionEvent } from "supertokens-node/framework/awsLambda"; -import { APIGatewayAuthorizerEvent, PolicyDocument, Statement, AuthResponse, StatementEffect } from "aws-lambda"; // REMOVE_FROM_OUTPUT -import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; -import middy from "@middy/core"; -import cors from "@middy/http-cors"; - -supertokens.init(getBackendConfig()); - -type AuthorizerEvent = SessionEvent & APIGatewayAuthorizerEvent; - -const lambdaHandler = async (event: AuthorizerEvent) => { - return { - body: JSON.stringify({ - sessionHandle: event.session?.getHandle(), - userId: event.session?.getUserId(), - accessTokenPayload: event.session?.getAccessTokenPayload(), - }), - statusCode: 200, - }; -}; - -export const handler = middy(verifySession(lambdaHandler)) - .use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) - ) - .onError((request) => { - throw request.error; - }); -``` - -Now, import this function in your `index.mjs` handler file as shown below: - -```tsx title="index.mjs" -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/awsLambda"; -// @ts-ignore -import { getBackendConfig } from "./config.mjs"; -import middy from "@middy/core"; -import cors from "@middy/http-cors"; -// highlight-start -// @ts-ignore -import { handler as userHandler } from "./user.mjs"; -// highlight-end - -supertokens.init(getBackendConfig()); - -export const handler = middy( - middleware((event) => { - - // highlight-start - if (event.path === "/user") { - return userHandler(event); - // highlight-end - } - - return { - body: JSON.stringify({ - msg: "Hello!", - }), - statusCode: 200, - }; - }) -) - .use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) - ) - .onError((request) => { - throw request.error; - }); -``` - -:::note -The `verifySession` middleware automatically returns a 401 Unauthorised error if the session is not valid. You can alter the default behaviour by passing `{ sessionRequired: false }` as the second argument to the `verifySession` middleware. - -If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `supertokens.init` and include the `Session` recipe in the `recipeList` for each respective lambda function. -::: - - - - -```python title="handler.py" -import nest_asyncio # type: ignore -nest_asyncio.apply() # type: ignore - -from fastapi import FastAPI -from starlette.middleware.cors import CORSMiddleware -from mangum import Mangum - -from supertokens_python import init, get_all_cors_headers -from supertokens_python.framework.fastapi import get_middleware - -import config # type: ignore - -init( - supertokens_config=config.supertokens_config, # type: ignore - app_info=config.app_info, # type: ignore - framework=config.framework, # type: ignore - recipe_list=config.recipe_list, # type: ignore - mode="asgi", -) - -app = FastAPI(title="SuperTokens Example") - -# highlight-start -from fastapi import Depends -from supertokens_python.recipe.session.framework.fastapi import verify_session -from supertokens_python.recipe.session import SessionContainer - -@app.get("/user") -def user(s: SessionContainer = Depends(verify_session())): - return { - "sessionHandle": s.get_handle(), - "userId": s.get_user_id(), - "accessTokenPayload": s.get_access_token_payload() - } -# highlight-end - -app.add_middleware(get_middleware()) - -app = CORSMiddleware( - app=app, - allow_origins=[ - config.app_info.website_domain # type: ignore - ], - allow_credentials=True, - allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], - allow_headers=["Content-Type"] + get_all_cors_headers(), -) - -handler = Mangum(app) -``` -:::note -The `verify_session` middleware automatically returns a 401 Unauthorised error if the session is not valid. You can alter the default behaviour by passing `session_required=False` the `verify_session` middleware. - -If each API route has its own lambda function, you can skip using the SuperTokens auth middleware. Instead, ensure to call `init` function and include the `session` recipe in the `recipe_list` for each respective lambda function. -::: - - - - - ---- - -## Using Lambda Authorizers - - -You can use a lambda as an authorizer in API Gateways. This will enable you to use SuperTokens in a lambda to authorize requests to other integrations (e.g., AppSync). An `Authorizer` pointed to this lambda will add `context.authorizer.principalId` that you can map to a header. For example, you can map this to an "x-user-id" header which will be set to the id of the logged-in user. If there is no valid session for the request, this header won't exist. - -### 1. Add configurations and dependencies - -Refer to the [frontend](/docs/quickstart/frontend-setup), [lambda layer](/docs/quickstart/integrations/aws-lambda/quickstart-guide#2-set-up-lambda-layer), and [lambda setup](/docs/quickstart/integrations/aws-lambda/quickstart-guide#3-set-up-lambda). - -### 2. Add code to the lambda function handler - - - - -Use the code below as the handler for the lambda. Remember that whenever we want to use any functions from the `supertokens-python` lib, we have to call the `init` function at the top of that serverless function file. We can then use `get_session()` to get the session. - -```python title="auth.py" -import nest_asyncio # type: ignore -import json -nest_asyncio.apply() # type: ignore - -from typing import Optional, Dict, Any - -from fastapi import FastAPI -from starlette.middleware.cors import CORSMiddleware -from mangum import Mangum - -from supertokens_python import init, get_all_cors_headers -from supertokens_python.framework.fastapi import get_middleware - -import config # type: ignore - -init( - supertokens_config=config.supertokens_config, # type: ignore - app_info=config.app_info, # type: ignore - framework=config.framework, # type: ignore - recipe_list=config.recipe_list, # type: ignore - mode="asgi", -) -app = FastAPI(title="SuperTokens Example") - -def generate_policy(principal_id: str, effect: str, resource: str, context: Optional[Dict[str, Any]]): - policy_document = { - "Version": "2012-10-17", - "Statement": [ - {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource} - ], - } - auth_response = { - "principalId": principal_id, - "policyDocument": policy_document, - "context": context or {}, - } - - return auth_response - - -def generate_allow(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): - return generate_policy(principal_id, "Allow", resource, context) - - -def generate_deny(principal_id: str, resource: str, context: Optional[Dict[str, Any]] = None): - return generate_policy(principal_id, "Deny", resource, context) - - -from fastapi import Request -from supertokens_python.recipe.session.syncio import get_session -from supertokens_python.recipe.session.exceptions import (InvalidClaimsError, - TryRefreshTokenError, - UnauthorisedError) - -@app.get("/{full_path:path}") -def handle_auth(request: Request, full_path: str): - event = request.scope["aws.event"] - method_arn = event.get("methodArn") - - try: - session = get_session(request, session_required=False) - if session: - return generate_allow(session.get_user_id(), method_arn) - else: - return generate_allow("", method_arn) - except Exception as e: - if isinstance(e, TryRefreshTokenError) or isinstance(e, UnauthorisedError): - raise Exception("Unauthorized") - if isinstance(e, InvalidClaimsError): - claim_validation_errors = [err.to_json() for err in e.payload] - return generate_deny( - "", - method_arn, - { - "body": { - "message": "invalid claims", - "claimValidationErrors": claim_validation_errors, - } - }, - ) - - raise e - - -app.add_middleware(get_middleware()) - -app = CORSMiddleware( - app=app, - allow_origins=[ - config.app_info.website_domain # type: ignore - ], - allow_credentials=True, - allow_methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], - allow_headers=["Content-Type"] + get_all_cors_headers(), -) - -def handler(event: Dict[str, Any], context: Any): - mangum_handler = Mangum(app) - response: Dict[str, Any] = mangum_handler(event, context) - - if event.get("methodArn"): - gateway_response = json.loads(response["body"]) - gateway_response["context"]["setCookie"] = response["headers"]["set-cookie"] - return gateway_response - - return response -``` - - - - -Use the code below as the handler for the lambda. -Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. -We can then use `getSession()` to get the session. - -```tsx title="index.mjs" -import supertokens from "supertokens-node"; -import { SessionEvent } from "supertokens-node/framework/awsLambda"; -import { APIGatewayAuthorizerEvent, PolicyDocument, Statement, AuthResponse, StatementEffect } from "aws-lambda"; // REMOVE_FROM_OUTPUT -import Session from "supertokens-node/recipe/session"; - -// @ts-expect-error -import { getBackendConfig } from "./config.mjs"; - -supertokens.init(getBackendConfig()); - -type AuthorizerEvent = SessionEvent & APIGatewayAuthorizerEvent; - -export const handler = async function (event: AuthorizerEvent) { - try { - const session = await Session.getSession(event, event, { sessionRequired: false }); - if (session) { - return generateAllow(session.getUserId(), event.methodArn, { - setCookie: event.supertokens.response.cookies.join(', '), - }); - } else { - return generateAllow("", event.methodArn, { - setCookie: event.supertokens.response.cookies.join(', '), - }); - } - } catch (ex: any) { - if (ex.type === "TRY_REFRESH_TOKEN" || ex.type === "UNAUTHORISED") { - throw new Error("Unauthorized"); - } - if (ex.type === "INVALID_CLAIMS") { - return generateDeny("", event.methodArn, { - body: JSON.stringify({ - message: "invalid claim", - claimValidationErrors: ex.payload, - }), - setCookie: event.supertokens.response.cookies.join(", "), - }); - } - throw ex; - } -} - -const generatePolicy = function (principalId: string, effect: StatementEffect, resource: string, context?: any) { - const policyDocument: PolicyDocument = { - Version: '2012-10-17', - Statement: [], - }; - - const statementOne: Statement = { - Action: 'execute-api:Invoke', - Effect: effect, - Resource: resource, - }; - - policyDocument.Statement[0] = statementOne; - - const authResponse: AuthResponse = { - principalId: principalId, - policyDocument: policyDocument, - context, - }; - - return authResponse; -} - -const generateAllow = function (principalId: string, resource: string, context?: any) { - return generatePolicy(principalId, 'Allow', resource, context); -}; - -const generateDeny = function (principalId: string, resource: string, context?: any) { - return generatePolicy(principalId, 'Deny', resource, context); -}; -``` - - - - -### 3. Configure the Authorizer - -- Go to the `Authorizers` tab in the API Gateway configuration -- Click **Create new Authorizer** and add it - - Fill the name field - - Set "Lambda function" to the one created above - - Set "Lambda Event Payload" to Request - - Delete the empty "Identity Source" - - Click "Create" - -### 4. Configure API Gateway - - -- In your API Gateway, create the resources and methods you require, enabling CORS if necessary (see [setup API gateway](/docs/quickstart/integrations/aws-lambda/quickstart-guide#1-set-up-api-gateway) for details) -- Select each method you want to enable the Authorizer and configure it to use the new `Authorizer` - - Click on "Method Request" - - Edit the "Authorization" field in Settings and set it to the one we just created. - - Go back to the method configuration and click on "Integration Request" - - Set up the integration you require (see [AppSync](/docs/quickstart/integrations/aws-lambda/appsync-integration) for an example) - - Add a header mapping to make use of the context set in the lambda. - - Open "HTTP Headers" - - Add all headers required (e.g., "x-user-id" mapped to "context.authorizer.principalId") - - Repeat for any values from the context you want to add as a Header - - - Go back to the method configuration and click on "Method Response" - - Open the dropdown next to the 200 status code - - Add the "Set-Cookie" header - - Add any other headers that should be present on the response. - - - Go back to the method configuration and click on "Integration Response" - - Open the dropdown - - Open "Header Mappings" - - Add "Set-Cookie" mapped to "context.authorizer.setCookie" -- In the API Gateway left menu, select "Gateway Responses" - - Select "Access Denied" - - Click "Edit" - - Add response headers: - - Add `Access-Control-Allow-Origin` with value `'^{appInfo.websiteDomain}'` - - Add `Access-Control-Allow-Credentials` with value `'true'`. **Don't miss out on those quotes else it won't get configured correctly.** - - Add "Set-Cookie" with value `context.authorizer.setCookie` **no quotes** - - Under response templates: - - Select `application/json`: - - Set "Response template body" to `$context.authorizer.body` - - Click "Save" - - Select "Unauthorized" - - Add response headers: - - Add `Access-Control-Allow-Origin` with value `'^{appInfo.websiteDomain}'` - - Add `Access-Control-Allow-Credentials` with value `'true'`. **Don't miss out on those quotes else it won't get configured correctly.** - - Click "Save" -- Deploy your API and test it - ---- - -## Using JWT Authorizers - -:::caution - -AWS supports JWT authorizers for HTTP APIs and not REST APIs on the API Gateway service. For REST APIs follow the [Lambda authorizer](/docs/quickstart/integrations/aws-lambda/session-verification#using-lambda-authorizers) guide - -This guide will work if you are using **SuperTokens Session Tokens**. - -If you implementing an **OAuth2** setup, through the [**Unified Login**](/docs/authentication/unified-login/introduction) or the [**Microservice Authentication**](/docs/authentication/m2m/client-credentials) features, you will have to manually set the token audience property. -Please check the referenced pages for more information. - -::: - -### 1. Add the `aud` claim in the JWT based on the authorizer configuration - - - - - - - -```tsx title="config.mjs" showAppTypeSelect - -import Session from 'supertokens-node/recipe/session' -import SuperTokensTypes from 'supertokens-node/types'; // REMOVE_FROM_OUTPUT - -export function getBackendConfig() { - return { - framework: "awsLambda", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/session/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - // highlight-start - Session.init({ - exposeAccessTokenToFrontendInCookieBasedAuth: true, - override: { - functions: function (originalImplementation) { - return { - ...originalImplementation, - createNewSession: async function (input) { - input.accessTokenPayload = { - ...input.accessTokenPayload, - /* - * AWS requires JWTs to contain an audience (aud) claim - * The value for this claim should be the same - * as the value you set when creating the - * authorizer - */ - aud: "jwtAuthorizers", - }; - - return originalImplementation.createNewSession(input); - }, - }; - } - }, - }), - ], - // highlight-end - isInServerlessEnv: true, - } -} -``` - - - - -```python title="config.py" showAppTypeSelect -from supertokens_python.recipe import session -from supertokens_python import ( - InputAppInfo, - SupertokensConfig, -) -from supertokens_python.recipe.session.interfaces import RecipeInterface as SessionRecipeInterface - -from typing import Any, Dict, Optional -from supertokens_python.types import RecipeUserId - -supertokens_config = SupertokensConfig( - connection_uri="^{coreInfo.uri}", - api_key="^{coreInfo.key}" -) - -app_info = InputAppInfo( - # learn more about this on https://supertokens.com/docs/session/appinfo - app_name="^{appInfo.appName}", - api_domain="^{appInfo.apiDomain}", - website_domain="^{appInfo.websiteDomain}", - api_base_path="^{appInfo.apiBasePath}", - website_base_path="^{appInfo.websiteBasePath}", - api_gateway_path="/dev", -) - -framework = "fastapi" - -# highlight-start -def override_session_functions(oi: SessionRecipeInterface) -> SessionRecipeInterface: - oi_create_new_session = oi.create_new_session - - async def create_new_session( - user_id: str, - recipe_user_id: RecipeUserId, - access_token_payload: Optional[Dict[str, Any]], - session_data_in_database: Optional[Dict[str, Any]], - disable_anti_csrf: Optional[bool], - tenant_id: str, - user_context: Dict[str, Any], - ): - # AWS requires JWTs to contain an audience (aud) claim - # The value for this claim should be the same as the - # value you set when creating the authorizer - - # highlight-next-line - if access_token_payload is None: - access_token_payload = {} - - access_token_payload["aud"] = "jwtAuthorizers" - return await oi_create_new_session(user_id, recipe_user_id, access_token_payload, session_data_in_database, disable_anti_csrf, tenant_id, user_context) - - oi.create_new_session = create_new_session - return oi -# highlight-end - -recipe_list = [ - # highlight-start - session.init( - override=session.InputOverrideConfig( - functions=override_session_functions, - ), - expose_access_token_to_frontend_in_cookie_based_auth=True, - ), - # highlight-end -] -``` - - - - - - - -### 2. Configure your authorizer - -- Go to the "Authorizers" tab in the API Gateway configuration and select the "Manage authorizers" tab -- Click "Create", in the creation screen select "JWT" as the "Authorizer type" -- Enter a name for your authorizer (You can enter any name for this field) -- Use `$request.header.Authorization` for the "Identity source". This means that API requests will contain the JWT as a Bearer token under the request header "Authorization". -- Use `^{appInfo.apiDomain}//^{appInfo.apiBasePath}` for the "Issuer URL". -- Set a value for the "Audience" field, this will be the value you expect the JWT to have under the `aud` claim. In the backend config above the value is set to `"jwtAuthorizers"` - -### 3. Add the authorizer to your API -- In the "Authorization" section select the "Attach authorizers to routes" tab -- Click on the route you want to add the authorizer to and select the authorizer you created from the dropdown -- Click "Attach authorizer" -- Deploy your changes and test your API - -### 4. Check for auth claims of the JWT - -Once the JWT authorizer successfully validates the JWT, the claims of the JWT will be available to your lambda functions via `$event.requestContext.authorizer.jwt.claims`. You should check for the right authorization access here. -For example, if one of your lambda functions requires that the user's email is verified, then it should check for the `jwt` payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. Similar checks need to be done to enforce the right user role or if 2FA is completed or not. -This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in the this flow, you will have to check them on your own. - - - diff --git a/docs/quickstart/integrations/graphql.mdx b/docs/quickstart/integrations/graphql.mdx deleted file mode 100644 index 39ebd49e58..0000000000 --- a/docs/quickstart/integrations/graphql.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: GraphQL -hide_title: true -sidebar_position: 3 -description: >- - Set up backend session verification in GraphQL resolvers using SuperTokens for - user authentication. -page_type: tutorial -category: quickstart ---- - -import OAuthVerifyTokensCallout from "../../_blocks/oauth-tokens-callout.mdx"; - -# GraphQL Session Verification - -## Before you start - -These instructions only show you how to perform **session verification** in a GraphQL context. -You will first have to go through the [quickstart guide](/docs/quickstart/frontend-setup) to configure **SuperTokens** and then return to this page. - - - -## Using the GraphQL context - -We want to use the `Session.getSession` function in the `context` function to verify the session, and add the `userId` into our context so that our resolvers can read it. -If the user id not logged in, we will set the `userId` to `undefined` in the context - -```tsx -import { ApolloServer } from "@apollo/server"; -import express from "express"; -import { expressMiddleware } from "@apollo/server/express4"; -import { GraphQLError } from 'graphql'; -import Session from "supertokens-node/recipe/session"; - -let app = express(); - -const typeDefs = '...' -const resolvers = {/* ... */ } - -const server = new ApolloServer({ - typeDefs, - resolvers, -}) - -server.start().then(() => { - app.use(express.json(), expressMiddleware(server, { - - // Note: This example uses the `req` and `res` argument to access headers, - // but the arguments received by `context` vary by integration. - // This means they vary for Express, Fastify, Lambda, etc. - context: async ({ req, res }) => { - // highlight-start - try { - let session = await Session.getSession(req, res, { - sessionRequired: false - }) - return { - userId: session !== undefined ? session.getUserId() : undefined - }; - } catch (err) { - if (Session.Error.isErrorFromSuperTokens(err)) { - throw new GraphQLError('Session related error', { - extensions: { - code: 'UNAUTHENTICATED', - http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, - }, - }); - } - throw err; - } - // highlight-end - }, - })) - - app.listen(3001, () => { - console.log("Server started"); - }) -}) -``` - -In the above code snippet, we first attempt to verify the session using the `Session.getSession` function. -If the session is valid, we will add the `userId` to the context. -If the access token has expired, we will throw an error with a status code of `401`. -If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. - -The `401` status code will cause the session refresh flow to start, which will give a new access token to the user, or else if the session was revoked, the user will be logged out. - -In case the user is not logged in, the `Session.getSession` function will throw return `undefined`, in which case, your resolvers will not have a `userId` in the context. - -The downside of this method is that if you want to mutate the session's access token payload in one of your resolvers, then you don't have access to the `session` object in there. This is where the method below comes into the picture: - -## Using the GraphQL resolver - -Unlike the method above, we will be doing session verification on a per resolver basis here. -This means that you will have access to the `session` object in your resolver using which you can update the information in the session (like its access token payload). - -We start by creating a helper function (a sort of middleware for your resolver) which you will have to call in all of your resolvers that require a session: - -```tsx -import Session, { SessionContainer } from "supertokens-node/recipe/session"; -import { GraphQLError } from 'graphql'; - -async function withSession(contextValue: any, resolver: (session: SessionContainer) => Promise) { - try { - let session = await Session.getSession(contextValue.req, contextValue.res); - return await resolver(session); - } catch (err) { - if (Session.Error.isErrorFromSuperTokens(err)) { - throw new GraphQLError('Session related error', { - extensions: { - code: 'UNAUTHENTICATED', - http: { status: err.type === Session.Error.INVALID_CLAIMS ? 403 : 401 }, - }, - }); - } - } -} -``` - -In the above function, we attempt to verify the session using `Session.getSession`. If the session is valid, we will call the `resolver` function with the `session` object. If the access token has expired, or if the session does not exist, we will throw an error with a status code of `401`. If a session claim has failed (for example if the user's email is not verified) we will return a status code of `403`. - -For this resolver to work, we will have to add the `req` and `res` object into the GraphQL context. This can be done as follows: - -```tsx -import { ApolloServer } from "@apollo/server"; -import express from "express"; -import { expressMiddleware } from "@apollo/server/express4"; -import { GraphQLError } from 'graphql'; - -let app = express(); - -const typeDefs = '...' -const resolvers = {/* ... */} - -const server = new ApolloServer({ - typeDefs, - resolvers, -}) - -server.start().then(() => { - app.use(express.json(), expressMiddleware(server, { - // Note: This example uses the `req` and `res` argument to access headers, - // but the arguments received by `context` vary by integration. - // This means they vary for Express, Fastify, Lambda, etc. - context: async ({req, res}) => { - // highlight-start - return { - req, res - }; - // highlight-end - }, - })) - - app.listen(3001, () => { - console.log("Server started"); - }) -}) -``` - -Finally, we can use our `withSession` in our resolvers as shown below: - -```tsx -import { ApolloServer } from "@apollo/server"; -declare let getUserName: any; // REMOVE_FROM_OUTPUT -declare let withSession: (contextValue: any, func: (sessoin: any) => Promise) => Promise; // REMOVE_FROM_OUTPUT -declare let typeDefs: any; // REMOVE_FROM_OUTPUT - -const server = new ApolloServer({ - typeDefs, - resolvers: { - Query: { - userProfile: async (_: any, __: any, contextValue) => { - // highlight-start - // starts of your resolver code.. - return await withSession(contextValue, async (session) => { - // getUserName is a custom application function... - let name = await getUserName(session.getUserId()) - - return { - userId: session.getUserId(), - sessionHandle: session.getHandle(), - name - }; - }); - // highlight-end - } - }, - }, -}) -``` - - diff --git a/docs/quickstart/integrations/hasura.mdx b/docs/quickstart/integrations/hasura.mdx deleted file mode 100644 index 36395bd97a..0000000000 --- a/docs/quickstart/integrations/hasura.mdx +++ /dev/null @@ -1,437 +0,0 @@ ---- -title: Hasura -hide_title: true -sidebar_position: 4 -toc_max_heading_level: 4 -description: Learn how to use SuperTokens with Hasura -page_type: tutorial -category: quickstart ---- - - -import OAuthVerifyTokensCallout from "../../_blocks/oauth-tokens-callout.mdx"; - - -# Hasura Guide - - -## Before you start - -The tutorial assumes that you already have a working application integrated with **SuperTokens**. -If you have not, please check the [Quickstart Guide](/docs/quickstart/introduction). - -Using SuperTokens with Hasura requires you to host your own API layer that uses our Backend SDK. -If you do not want to host your own server you can use a serverless environment to achieve this. - - - - -## Steps - -### 1. Expose the access token to the frontend - -For cookie based auth, the access token is not available on the frontend by default. -In order to expose it, you need to set the `exposeAccessTokenToFrontendInCookieBasedAuth` config to `true`. - - - - -```tsx -import SuperTokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - supertokens: { - connectionURI: "..." - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - //highlight-start - exposeAccessTokenToFrontendInCookieBasedAuth: true - //highlight-end - }) - ] -}); -``` - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - session.Init(&sessmodels.TypeInput{ - //highlight-start - ExposeAccessTokenToFrontendInCookieBasedAuth: true, - //highlight-end - }), - }, - }) -} -``` - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import session - -init( - app_info=InputAppInfo( - api_domain="...", app_name="...", website_domain="..."), - framework='...', # type: ignore - recipe_list=[ - session.init( - # highlight-next-line - expose_access_token_to_frontend_in_cookie_based_auth=True, - ) - ] -) -``` - - - - -### 2. Add custom claims to the JWT - -Hasura requires claims to be set in a specific way, read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html) to know more. - - - - -```tsx -import SuperTokens from "supertokens-node"; -import Session from "supertokens-node/recipe/session"; - -SuperTokens.init({ - supertokens: { - connectionURI: "...", - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - exposeAccessTokenToFrontendInCookieBasedAuth: true, - override: { - functions: function (originalImplementation) { - return { - ...originalImplementation, - createNewSession: async function (input) { - //highlight-start - input.accessTokenPayload = { - ...input.accessTokenPayload, - "https://hasura.io/jwt/claims": { - "x-hasura-user-id": input.userId, - "x-hasura-default-role": "user", - "x-hasura-allowed-roles": ["user"], - } - }; - //highlight-end - - return originalImplementation.createNewSession(input); - }, - }; - } - }, - }) - ] -}); -``` - - - -```go -import ( - "github.com/supertokens/supertokens-golang/recipe/session" - "github.com/supertokens/supertokens-golang/recipe/session/sessmodels" - "github.com/supertokens/supertokens-golang/supertokens" -) - -func main() { - supertokens.Init(supertokens.TypeInput{ - RecipeList: []supertokens.Recipe{ - session.Init(&sessmodels.TypeInput{ - ExposeAccessTokenToFrontendInCookieBasedAuth: true, - Override: &sessmodels.OverrideStruct{ - Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { - - originalCreateNewSession := *originalImplementation.CreateNewSession - - (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload map[string]interface{}, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - if accessTokenPayload == nil { - accessTokenPayload = map[string]interface{}{} - } - - //highlight-start - hasuraClaims := map[string]interface{}{ - "x-hasura-user-id": userID, - "x-hasura-default-role": "user", - "x-hasura-allowed-roles": []string{"user"}, - } - - accessTokenPayload["https://hasura.io/jwt/claims"] = hasuraClaims - //highlight-end - - return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) - } - - return originalImplementation - }, - }, - }), - }, - }) -} -``` - - - -```python -from supertokens_python import init, InputAppInfo -from supertokens_python.recipe import session -from supertokens_python.recipe.session.interfaces import RecipeInterface -from typing import Dict, Optional, Any -from supertokens_python.types import RecipeUserId - - -def override_functions(original_implementation: RecipeInterface): - original_implementation_create_new_session = ( - original_implementation.create_new_session - ) - - async def create_new_session( - user_id: str, - recipe_user_id: RecipeUserId, - access_token_payload: Optional[Dict[str, Any]], - session_data_in_database: Optional[Dict[str, Any]], - disable_anti_csrf: Optional[bool], - tenant_id: str, - user_context: Dict[str, Any], - ): - - if access_token_payload is None: - access_token_payload = {} - - access_token_payload["https://hasura.io/jwt/claims"] = { - "x-hasura-user-id": user_id, - "x-hasura-default-role": "user", - "x-hasura-allowed-roles": ["user"], - } - - return await original_implementation_create_new_session( - user_id, - recipe_user_id, - access_token_payload, - session_data_in_database, - disable_anti_csrf, - tenant_id, - user_context, - ) - - original_implementation.create_new_session = create_new_session - return original_implementation - - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore - recipe_list=[ - session.init( - override=session.InputOverrideConfig(functions=override_functions), - expose_access_token_to_frontend_in_cookie_based_auth=True, - ) - ], -) -``` - - - - -### 3. Configure Hasura environment variables - -:::info -Read the [official documentation](https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt.html#configuring-jwt-mode) to know about setting the JWT secret environment variable on Hasura -::: - -To use JWT based authentication, Hasura requires setting environment variables when configuring your app. -With SuperTokens this can be done in 2 ways: - -#### Using the JWKS endpoint - -When configuring Hasura, you can set the `jwk_url` property. - -```json -{ - "jwk_url": "^{appInfo.apiDomain}/^{appInfo.apiBasePath}/jwt/jwks.json" -} -``` - -You can get the JWKS URL for your backend by using the method explained [here](/docs/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library) - -#### Using a key string - -Hasura let's you provide a PEM string in the configuration. -Refer to [this page](/docs/additional-verification/session-verification/protect-api-routes##with-the-public-key-string) to know how to get a public key as a string, you can then use that key string in the Hasura config: - -```json -{ - "type": "RS256", - "key": "CERTIFICATE_STRING", -} -``` - -### 4. Check for claim values in Hasura - -Some checks like if the email is verified, or if 2FA is completed are stored as claim values in the JWT. -You should check for the values of these claims in your GraphQL functions wherever required. -For example, if one of your GraphQL functions requires that the user's email is verified, then it should check for the JWT payload's `st-ev` claim value to be `{v: true, t:...}`, else it should reject the request. - -You can also use a [custom Hasura authorizer webhook](https://hasura.io/docs/latest/auth/authentication/webhook/) to check for the values of these claims depending on your app's requirements. - -This is required because SuperTokens issues JWTs immediately after the user signs up / logs in, regardless of if all the authorisation checks pass or not. -Functions exposed by our SDK like `verifySession` or `getSession` do these authorisation checks on their own, but since these functions are not used in the Hasura flow, you will have to check them on your own. - -### 5. Make requests to Hasura - -#### 5.1 Get the JWT on the frontend - - - - - - - - - -```tsx -import Session from "supertokens-web-js/recipe/session"; - -async function getToken(): Promise { - // highlight-next-line - const accessToken = await Session.getAccessToken(); - console.log(accessToken); -} -``` - - - - -```tsx -import supertokensSession from "supertokens-web-js-script/recipe/session"; -import supertokensEmailVerification from "supertokens-web-js-script/recipe/emailverification"; -async function getToken(): Promise { - // highlight-next-line - const accessToken = await supertokensSession.getAccessToken(); - console.log(accessToken); -} -``` - - - - - - - - - - -```tsx -import SuperTokens from 'supertokens-react-native'; - -async function getToken(): Promise { - // highlight-next-line - const accessToken = await SuperTokens.getAccessToken(); - console.log(accessToken); -} -``` - - - - - -```kotlin -import android.app.Application -import com.supertokens.session.SuperTokens - -class MainApplication: Application() { - fun getToken(): String { - return SuperTokens.getAccessToken(applicationContext) - } -} -``` - - - - - -```swift -import UIKit -import SuperTokensIOS - -fileprivate class ViewController: UIViewController { - func getToken() -> String? { - return SuperTokens.getAccessToken() - } -} -``` - - - - - -```dart -import 'package:supertokens_flutter/supertokens.dart'; - -Future getToken() async { - return await SuperTokens.getAccessToken(); -} -``` - - - - - - - - -#### 5.2 Make an HTTP requests - -```tsx -import axios from "axios"; - -async function makeRequest() { - let url = "..."; - let jwt = "..."; // Refer to step 5.a - let response = await axios.get(url, { - // highlight-start - headers: { - "Authorization": `Bearer ${jwt}`, - }, - // highlight-end - }); -} -``` - -## Local development - - -If you are using Hasura cloud and testing your backend APIs in your local environment, JWT verification will fail because Hasura will not be able to query the JWKS endpoint (because the cloud can not query your local environment i.e localhost, 127.0.0.1). - -To solve this problem you will need to expose your locally hosted backend APIs to the internet. For example you can use [ngrok](https://ngrok.com/). -After that, you need to configure Hasura to use the `/^{appInfo.apiBasePath}/jwt/jwks.json` as the JWKS endpoint (explained in [step 4](#4-configure-hasura-environment-variables)) diff --git a/docs/quickstart/integrations/nestjs.mdx b/docs/quickstart/integrations/nestjs.mdx deleted file mode 100644 index 8008fc9b69..0000000000 --- a/docs/quickstart/integrations/nestjs.mdx +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: NestJS -hide_title: true -sidebar_position: 5 -toc_max_heading_level: 4 -description: >- - Integrate SuperTokens with NestJS backend, covering modules, services, - middleware, and session management. -page_type: tutorial -category: quickstart ---- - - - - - -# NestJS guide - -## Overview - - -Integrating SuperTokens into a NestJS backend differs in some aspects from the main quickstart guide. -That's because of the additional framework specific entities that are involved. -To aid the process you can use the `supertokens-nestjs` package which exposes abstractions that speed up the setup. - -## Before you start - -This guide assumes that you have already completed the [main quickstart guide](/docs/quickstart/introduction). -If not, please go through it before continuing with this page. -You need to first understand how to configure the required recipes and run a sample project. - -You can also explore the [example projects](https://github.com/supertokens/supertokens-nestjs/tree/main/examples) for complete code references on how to use the libraries. - -## Steps - -### 1. Install the required packages - -```bash -npm i -s supertokens-node supertokens-nestjs -``` - -### 2. Initialize the `SuperTokensModule` - -Inside your main application module, initialize the **SuperTokensModule** with your required configuration. - -```tsx -import { Module } from '@nestjs/common' -import { SuperTokensModule } from 'supertokens-nestjs' - -@Module({ - imports: [ - SuperTokensModule.forRoot({ - // Choose between 'express' and 'fastify' - // If you are using fastify make sure to also set the fastifyAdapter property - framework: 'express', - supertokens: { - connectionURI: '...', - }, - appInfo: { - appName: '...', - apiDomain: '...', - websiteDomain: '...', - }, - recipeList: [ - /* ... */ - ], - }), - ], - controllers: [ - /* ... */ - ], - providers: [ - /* ... */ - ], -}) -export class AppModule {} -``` - - -:::info Tip -You can use the `SuperTokensModule.forRootAsync` if you want to load the configuration asynchronously. -::: - -### 3. Update the `bootstrap` function - -Inside your `bootstrap` function, you have to update the CORS configuration and set the exception filter. -**SuperTokens** generates a set of CORS headers that the authentication flow requires. -And, the global filter ensures that all authentication related errors get handled by the SDK. - -```tsx -import supertokens from 'supertokens-node' -import { SuperTokensExceptionFilter } from 'supertokens-nestjs' -import { NestFactory } from '@nestjs/core' -// @ts-expect-error -import { AppModule } from './app.module' -// @ts-expect-error -import { appInfo } from './config' - - -async function bootstrap() { - const app = await NestFactory.create(AppModule) - app.enableCors({ - origin: [appInfo.websiteDomain], - allowedHeaders: ['content-type', ...supertokens.getAllCORSHeaders()], - credentials: true, - }) - app.useGlobalFilters(new SuperTokensExceptionFilter()) - - await app.listen(3001) -} -``` -### 4. Add the `SuperTokensAuthGuard` - -The `SuperTokensAuthGuard` automatically marks the routes that it targets as protected. -By default session validation gets performed based on the default configuration provided in the `Session.init` call. -You can customize the validation logic with decorators. -More on that in the next step. - -#### As a global guard - -This applies the `SuperTokensAuthGuard` to all routes in exposed by controllers registered in that module. - -```tsx -import { Module } from '@nestjs/common' -import { APP_GUARD } from '@nestjs/core' -import { SuperTokensAuthGuard } from 'supertokens-nestjs' - -@Module({ - imports: [ - /* ... */ - ], - controllers: [ - /* ... */ - ], - providers: [ - { - provide: APP_GUARD, - useClass: SuperTokensAuthGuard, - }, - ], -}) -export class AppModule {} -``` - -#### As a controller guard - -This applies the `SuperTokensAuthGuard` only to the routes defined in the controller. - -```tsx -import { Controller, UseGuards } from '@nestjs/common' -import { SuperTokensAuthGuard } from 'supertokens-nestjs' - -@Controller() -@UseGuards(SuperTokensAuthGuard) -export class AppController {} -``` - -### 5. Manage authentication with decorators - -The `supertokens-nestjs` package exposes two sets of decorators: -- Function decorators like `VerifySession` and `PublicAccess` that you can use on controller methods to customize the session validation logic. -- Parameter decorators like `Session` that you can use to access the session data in your controller methods. - - -```tsx -import { Controller, Delete, Get, Patch, Post } from '@nestjs/common' -import { PublicAccess, Session, VerifySession } from 'supertokens-nestjs' -import { SessionContainer } from "supertokens-node/recipe/session"; - -@Controller() -class AppController { - @Get('/user') - @VerifySession() - async getUserInfo(@Session('userId') userId: string) {} - - @Get('/user/:userId') - @VerifySession({ - roles: ['admin'], - }) - async deleteUser(@Session() session: SessionContainer) {} - - @Get('/user/profile') - @PublicAccess() - async getUserProfile() {} -} -``` - -:::info tip -With the `VerifySession` decorator, you can specify the following options: -| Option | Type | Description | -|--------|------|-------------| -| `roles` | `string[]` | Roles that the user must have to access the route | -| `permissions` | `string[]` | Permissions that the user must have to access the route | -| `requiresMfa` | `boolean` | Indicates whether the user must have MFA enabled to access the route | -| `requireEmailVerification` | `boolean` | Indicates whether the user must have their email verified to access the route | -| `options` | `VerifySessionOptions` | The value that normally passed to the `getSession` or `verifySession` functions. Use it if you want additional levels of customization. | - -::: - - -### 6. Configure SuperTokens core - - { - return ( - Are you using https://try.supertokens.com as the connection URI in the init function? - ) - }} defaultAnswer="Yes"> - - -You need to setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: -- [Managed service](/docs/quickstart/next-steps#configure-the-core-service) -- [Self hosted](/docs/deployment/self-host-supertokens) - - - - - -:::success -You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section. -::: - - - diff --git a/docs/quickstart/integrations/netlify.mdx b/docs/quickstart/integrations/netlify.mdx deleted file mode 100644 index 2d1e385af0..0000000000 --- a/docs/quickstart/integrations/netlify.mdx +++ /dev/null @@ -1,196 +0,0 @@ ---- -title: Netlify -hide_title: true -sidebar_position: 6 -toc_max_heading_level: 4 -description: >- - Learn to set up SuperTokens authentication with Netlify using serverless - functions. -page_type: tutorial -category: quickstart ---- - - -import OAuthVerifyTokensCallout from "../../_blocks/oauth-tokens-callout.mdx"; - -# Netlify Guide - - -## Overview - -The following guide gets you though how to add SuperTokens to a Netlify serverless API. -You can also check out the [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-netlify) for a full working example. - -## Before you start - -This guide assumes that you are using Netlify for hosting your serverless API functions. -If this is not the case, and you are only hosting your frontend using Netlify, please follow the [Quick setup guide](/docs/quickstart/frontend-setup) instead. - -## Steps - -### 1. Setup the frontend - -Follow the [initial quickstart guide](/docs/quickstart/frontend-setup) to configure the frontend. - -### 2. Setup the backend - -#### 2.1 Install the SuperTokens node package - -```bash -npm i supertokens-node -``` - -#### 2.2 Create a configuration file - -Create a `config` folder in the root directory of your project. -Create a `supertokensConfig.js` inside the `config` folder. -An example of this file can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/config/supertokensConfig.js). - -#### 2.3 Create a backend configuration function - - - -```tsx title="/config/supertokensConfig.ts" showAppTypeSelect - -import EmailPassword from 'supertokens-node/recipe/emailpassword'; -import Session from 'supertokens-node/recipe/session' - -function getBackendConfig() { - return { - framework: "awsLambda", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo: { - // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}", - }, - recipeList: [ - EmailPassword.init(), - Session.init(), - ], - isInServerlessEnv: true, - } -} - -module.exports.getBackendConfig = getBackendConfig; - -``` - - -### 3. Expose the authentication APIs - -We will add all the backend APIs for auth on `/.netlify/functions/auth/*`. -This can be changed by setting the `apiBasePath` property in the `appInfo` object on the backend and frontend. -For the rest of this page, we will assume you are using `/.netlify/functions/auth/*`. - -#### 3.1 Create the `netlify/functions/auth.js` page - -Be sure to create the `netlify/functions/` folder. -An example of this can be found [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/auth.js). - -```tsx title="netlify/functions/auth.ts" -import supertokens from "supertokens-node"; -import { middleware } from "supertokens-node/framework/awsLambda"; -import middy from "@middy/core"; -import cors from "@middy/http-cors"; -// @ts-ignore -import { getBackendConfig } from "../../config/supertokensConfig"; - -supertokens.init(getBackendConfig()); - -module.exports.handler = middy(middleware(async (event, context) => { - if (event.httpMethod === "OPTIONS") { - return { - statusCode: 200, - body: "" - } - } - - return { - statusCode: 404, - body: "Not Found", - } -})).use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) -).onError(request => { - throw request.error; -}); -``` - -:::important -- Notice that we called `supertokens.init` above. We will need to call this in all API endpoints that use any functions related to SuperTokens. -- `CORS` is only needed if you are hosting your frontend using a separate domain (if your website domain is different that your API's domain). -::: - -#### 3.2 Use the login widget -If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) - -### 4. Add session verification - - - -For this guide, we will assume that we want an API `/.netlify/functions/user GET` which returns the current session information. - -#### 4.1 Create a new file `netlify/functions/user.js` - -An example of this is [here](https://github.com/supertokens/supertokens-auth-react/blob/master/examples/with-netlify/netlify/functions/user.js). - -#### 4.2 Call the `supertokens.init` function - -Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. - -```tsx title="netlify/functions/user.ts" -import supertokens from "supertokens-node"; -// @ts-ignore -import { getBackendConfig } from "../../config/supertokensConfig"; - -supertokens.init(getBackendConfig()) -``` - -#### 4.3 Use session verification with your API handler - -We use the `verifySession()` middleware to verify a session. - -```tsx title="netlify/functions/user.ts" -import supertokens from "supertokens-node"; -import { verifySession } from "supertokens-node/recipe/session/framework/awsLambda"; -import { SessionEvent } from "supertokens-node/framework/awsLambda" -import middy from "@middy/core"; -import cors from "@middy/http-cors"; -// @ts-ignore -import { getBackendConfig } from "../../config/supertokensConfig"; - -supertokens.init(getBackendConfig()); - -const handler = async (event: SessionEvent) => { - return { - body: JSON.stringify({ - sessionHandle: event.session!.getHandle(), - userId: event.session!.getUserId(), - accessTokenPayload: event.session!.getAccessTokenPayload(), - }), - }; -}; - -module.exports.handler = middy(verifySession(handler)).use( - cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...supertokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE", - }) -).onError(request => { - throw request.error; -}); -``` diff --git a/docs/quickstart/integrations/nextjs/_category_.json b/docs/quickstart/integrations/nextjs/_category_.json deleted file mode 100644 index a8490532b0..0000000000 --- a/docs/quickstart/integrations/nextjs/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "NextJS", - "position": 6 -} diff --git a/docs/quickstart/integrations/nextjs/app-directory/_category_.json b/docs/quickstart/integrations/nextjs/app-directory/_category_.json deleted file mode 100644 index c2cfb08191..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "App Directory", - "position": 1 -} diff --git a/docs/quickstart/integrations/nextjs/app-directory/about.mdx b/docs/quickstart/integrations/nextjs/app-directory/about.mdx deleted file mode 100644 index f632b77a2e..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/about.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -id: about -title: About -hide_title: true -show_ui_switcher: true -sidebar_position: 1 -description: >- - Integrate SuperTokens with Next.js for authentication, session verification, - and route protection. -page_type: tutorial -category: quickstart ---- - - - - - - -# Overview steps - -Integrating SuperTokens with a Next.js app involves: -- Calling the frontend and backend init functions -- Adding a website page to display the auth related widgets (on `/auth` by default) -- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) -- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page -- Performing session verification: - - In your APIs - - In your frontend routes - -## Try an example app -Download and run an example Next.js app quickly using the following command: - -```bash -npx create-supertokens-app@latest --frontend=next --recipe=emailpassword -``` - -## Follow our video guide - - - - - - - -# Overview steps - -Integrating SuperTokens with a Next.js app involves: -- Calling the frontend and backend init functions -- Building the various auth flows as per the [custom UI setup guide](../../custom-ui/init/frontend). -- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) -- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page -- Performing session verification: - - In your APIs - - In your frontend routes - -## Try an example app - -Download and run an example Next.js app quickly using the following command: - -```bash -npx create-supertokens-app@latest --frontend=next --recipe=emailpassword -``` - -:::note -This example app uses our pre-built UI -::: - - diff --git a/docs/quickstart/integrations/nextjs/app-directory/init.mdx b/docs/quickstart/integrations/nextjs/app-directory/init.mdx deleted file mode 100644 index 42f1050835..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/init.mdx +++ /dev/null @@ -1,315 +0,0 @@ ---- -id: init -title: 1. Configuration -hide_title: true -show_ui_switcher: true -sidebar_position: 2 -description: >- - Configure SuperTokens for authentication in your Next.js app with frontend and - backend setup. -page_type: tutorial -category: quickstart ---- - - -# 1. Configuration - - - - - -## 1. Install `supertokens` package -```bash -yarn add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors -``` - -## 2. Create configuration files -- Create a `config` folder in the app directory of your project. -- Create an `appInfo.ts` inside the `config` folder. -- Create a `backend.ts` inside the `config` folder. -- Create a `frontend.ts` inside the `config` folder. - -## 3. Create the `appInfo` configuration. - - - -```tsx title="app/config/appInfo.ts" - -export const appInfo = { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" -} - -``` - - - - - -## 1. Install `supertokens` package -```bash -yarn add supertokens-node supertokens-web-js nextjs-cors -``` - -## 2. Create configuration files -- Create a `config` folder in the app directory of your project -- Create an `appInfo.ts` inside the `config` folder. -- Create a `backend.ts` inside the `config` folder. -- Create a `frontend.ts` inside the `config` folder. - -## 3. Create the `appInfo` configuration. - - - -```tsx title="app/config/appInfo.ts" - -export const appInfo = { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - websiteDomain: "^{appInfo.websiteDomain}", - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", -} - -``` - - - - - - -## 4. Create a frontend config function - -```tsx title="app/config/frontend.tsx" -import EmailPasswordReact from 'supertokens-auth-react/recipe/emailpassword' -import SessionReact from 'supertokens-auth-react/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' -import { useRouter } from "next/navigation"; -import { SuperTokensConfig } from 'supertokens-auth-react/lib/build/types' - -const routerInfo: { router?: ReturnType; pathName?: string } = - {}; - -export function setRouter( - router: ReturnType, - pathName: string, -) { - routerInfo.router = router; - routerInfo.pathName = pathName; -} - -export const frontendConfig = (): SuperTokensConfig => { - return { - appInfo, - recipeList: [ - EmailPasswordReact.init(), - SessionReact.init(), - ], - windowHandler: (original) => ({ - ...original, - location: { - ...original.location, - getPathName: () => routerInfo.pathName!, - assign: (url) => routerInfo.router!.push(url.toString()), - setHref: (url) => routerInfo.router!.push(url.toString()), - }, - }), - } -} -``` - - - - - -## 4. Create a frontend config function - -```tsx title="app/config/frontend.tsx" -import EmailPasswordWebJs from 'supertokens-web-js/recipe/emailpassword' -import SessionWebJs from 'supertokens-web-js/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' -import { SuperTokensConfig } from "supertokens-web-js/types" - -export const frontendConfig = (): SuperTokensConfig => { - return { - appInfo, - recipeList: [ - EmailPasswordWebJs.init(), - SessionWebJs.init(), - ], - } -} -``` - - - - -## 5. Create a backend config function - -```tsx title="app/config/backend.ts" showAppTypeSelect -import SuperTokens from "supertokens-node"; -import EmailPasswordNode from 'supertokens-node/recipe/emailpassword' -import SessionNode from 'supertokens-node/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' -import { TypeInput } from "supertokens-node/types"; - -export const backendConfig = (): TypeInput => { - return { - framework: "custom", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo, - recipeList: [ - EmailPasswordNode.init(), - SessionNode.init(), - ], - isInServerlessEnv: true, - } -} - -let initialized = false; -// This function is used in your APIs to make sure SuperTokens is initialised -export function ensureSuperTokensInit() { - if (!initialized) { - SuperTokens.init(backendConfig()); - initialized = true; - } -} -``` - -`ensureSuperTokensinit` is a helper function that can be used in your API routes to make sure SuperTokens is initialised before using any functionality exposed by the backend SDKs. - - - - -## 6. Call the frontend `init` functions and wrap with `` component - -- Create a client component `/app/components/supertokensProvider.tsx`. This file will initialise SuperTokens and wrap its children with the `SuperTokensWrapper` component -- Modify the `/app/layout.tsx` file to use the `SuperTokensProvider` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). -- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/layout.tsx) - -```tsx title="/app/components/supertokensProvider.tsx" -'use client'; -declare let frontendConfig: any; // REMOVE_FROM_OUTPUT -declare let setRouter: any; // REMOVE_FROM_OUTPUT -import React from 'react'; -import { SuperTokensWrapper } from 'supertokens-auth-react'; -import SuperTokensReact from 'supertokens-auth-react'; -// @ts-ignore -import { frontendConfig, setRouter } from '../config/frontend'; -import { usePathname, useRouter } from 'next/navigation'; - -if (typeof window !== 'undefined') { - // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' - SuperTokensReact.init(frontendConfig()); -} - -export const SuperTokensProvider: React.FC> = ({ - children, -}) => { - setRouter(useRouter(), usePathname() || window.location.pathname); - - return {children}; -}; -``` - -```tsx title="/app/layout.tsx" -declare let SuperTokensProvider: any; // REMOVE_FROM_OUTPUT -import './globals.css' -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -// @ts-ignore -import { SuperTokensProvider } from "./components/supertokensProvider"; - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - {children} - - - ) -} -``` - - - - - -## 7. Call the frontend `init` functions and wrap with `` component - -- Create a client component `/app/components/supertokensInit.tsx`. This file will initialise SuperTokens. -- Modify the `/app/layout.tsx` file to use the `SuperTokensInit` component. You can learn more about this file [here](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required). - -```tsx title="/app/components/supertokensInit.tsx" -'use client'; -import React from 'react'; -import SuperTokensWebJs from 'supertokens-web-js' -// @ts-ignore -import { frontendConfig } from '../config/frontend'; - -if (typeof window !== 'undefined') { - // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' - SuperTokensWebJs.init(frontendConfig()); -} - -export const SuperTokensInit: React.FC> = ({ - children, -}) => { - return <>{children}; -}; -``` - -```tsx title="/app/layout.tsx" -declare let frontendConfig: () => any; // REMOVE_FROM_OUTPUT -import './globals.css' -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -// @ts-ignore -import { frontendConfig } from '../config/frontend'; -// @ts-ignore -import { SuperTokensInit } from "./components/supertokensInit"; - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - {children} - - - ) -} -``` - - diff --git a/docs/quickstart/integrations/nextjs/app-directory/next-steps.mdx b/docs/quickstart/integrations/nextjs/app-directory/next-steps.mdx deleted file mode 100644 index 2059d25ac3..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/next-steps.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: next-steps -title: 7. Next steps -hide_title: true -sidebar_position: 8 -pagination_next: null -description: >- - Set up SuperTokens core for your app using managed service or self-hosting - options. -page_type: tutorial -category: quickstart ---- - - -# 7. Next steps - -## Setting up the core and database - { - return ( - Are you using https://try.supertokens.com as the connection URI in the init function? - ) - }} defaultAnswer="Yes"> - - -You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: -- [Managed service](/docs/quickstart/next-steps#configure-the-core-service) -- [Self hosted](/docs/deployment/self-host-supertokens) - - - - -:::success -You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section. -::: - - - diff --git a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/_category_.json b/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/_category_.json deleted file mode 100644 index e08ee8e3e6..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "5. Checking Sessions in API Routes", - "position": 6 -} diff --git a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx b/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx deleted file mode 100644 index f4290847b1..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-middleware.mdx +++ /dev/null @@ -1,102 +0,0 @@ ---- -id: session-verification-middleware -title: Using the Next.js middleware -hide_title: true -sidebar_position: 2 -description: >- - Implement session verification in Next.js middleware to manage user - authentication and retrieve user IDs. -page_type: tutorial -category: quickstart ---- - - -# Using the Next.js middleware - - - -:::important -This method is an alternative method for using sessions in an API. If you are already using [session guards](./session-verification-session-guard.mdx), you can skip this step. -::: - -## Setting up the middleware - -In the middleware we check if a session exists using the `withSession` helper function and set the user's user id to the request headers using the session object. You can set other information in the same way. - -:::caution -You cannot pass the full session container through the middleware because the Next.js middleware does not allow objects to be passed. If you need to access the full session container in your APIs switch to using [session guards](./session-verification-session-guard.mdx). -::: - -```tsx title="middleware.ts" -import { withSession } from "supertokens-node/nextjs"; -import { NextResponse } from 'next/server'; -import type { NextRequest } from 'next/server'; -import { SessionContainer } from 'supertokens-node/recipe/session'; -// @ts-ignore -import { ensureSuperTokensInit } from "./app/config/backend"; - -ensureSuperTokensInit(); - -export async function middleware( - request: NextRequest & { session?: SessionContainer } -) { - if (request.headers.has("x-user-id")) { - console.warn("The FE tried to pass x-user-id, which is only supposed to be a backend internal header. Ignoring."); - request.headers.delete("x-user-id"); - } - - if (request.nextUrl.pathname.startsWith('/api/auth')) { - /** - * /api/auth/* endpoints are exposed by the SuperTokens SDK, - * we do not want to modify the request for these routes - */ - return NextResponse.next() - } - - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - if (session === undefined) { - return NextResponse.next() - } - return NextResponse.next({ - headers: { - // You cannot attach the full session object here - 'x-user-id': session.getUserId(), - }, - }) - }) -} - -export const config = { - matcher: '/api/:path*', -} -``` - -## Fetching the user ID in your APIs - -The middleware will run for all routes, we can read information set by the middleware in the API routes: - -```tsx title="app/api/userid/route.ts" -import { NextResponse, NextRequest } from "next/server"; -// @ts-ignore -import { ensureSuperTokensInit } from '../../config/backend'; - -ensureSuperTokensInit(); - -export function GET(request: NextRequest) { - const userId = request.headers.get("x-user-id"); - - // The middleware only adds the userId if a session exists - if (userId === null) { - return new NextResponse("Authentication required", { status: 401 }); - } - - return NextResponse.json({ - userId, - }); -} -``` - -This creates a `GET` request for the `/api/userid` route which returns the user id of the currently logged in user. diff --git a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx b/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx deleted file mode 100644 index c340d4065b..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: session-verification-session-guard -title: Adding a session guard to each API route -hide_title: true -sidebar_position: 1 -description: >- - Implement session guards for API routes to manage user authentication and - session validation. -page_type: tutorial -category: quickstart ---- - - -# Adding a session guard to each API route - - - -:::note -This is applicable for when the frontend calls an API in the `/app/api` folder. -::: - -For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. - -Create a new file `/app/api/user/route.ts` - -- An example of this is [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/user/route.ts). - -```ts title="app/api/user/route.ts" -import { withSession } from "supertokens-node/nextjs"; -import { NextResponse, NextRequest } from "next/server"; -// @ts-ignore -import { ensureSuperTokensInit } from '../../config/backend'; - -ensureSuperTokensInit(); - -export function GET(request: NextRequest) { - return withSession(request, async (err, session) => { - if (err) { - return NextResponse.json(err, { status: 500 }); - } - if (!session) { - return new NextResponse("Authentication required", { status: 401 }); - } - - return NextResponse.json({ - note: "Fetch any data from your application for authenticated user after using verifySession middleware", - userId: session.getUserId(), - sessionHandle: session.getHandle(), - accessTokenPayload: session.getAccessTokenPayload(), - }); - }); -} -``` - -In the above snippet we are creating a `GET` handler for the `/api/user` route. We call the `withSession` helper function. The function will pass the session object in the callback which we then use to read user information. If a session does not exist `undefined` will be passed instead. - -The `withSession` guard will return: -- Status `401` if the session does not exist or has expired -- Status `403` if the session claims fail their validation. For example if email verification is required but the user's email is not verified. diff --git a/docs/quickstart/integrations/nextjs/app-directory/protecting-route.mdx b/docs/quickstart/integrations/nextjs/app-directory/protecting-route.mdx deleted file mode 100644 index fdb5c31c0e..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/protecting-route.mdx +++ /dev/null @@ -1,507 +0,0 @@ ---- -id: protecting-route -title: 4. Checking for sessions in frontend routes -hide_title: true -sidebar_position: 5 -description: >- - Learn to protect frontend routes by checking user sessions and handling - session-related events. -page_type: tutorial -category: quickstart ---- - - -# 4. Checking for sessions in frontend routes - - - -Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page. - - - - - -## Sessions with Client Components - -Lets create a client component for the `/` route of our website. - -### Using the `SessionAuth` wrapper component - -```tsx title="app/components/homeClientComponent.tsx" -'use client' - -import { SessionAuth } from "supertokens-auth-react/recipe/session" - -export const HomeClientComponent = () => { - return ( - -
- Hello world -
-
- ); -} -``` - -`SessionAuth` is a component exposed by the SuperTokens React SDK, it checks if a session exists and if it does not exist it will redirect the user to the login page. It also does session claim checking on the frontend and take appropriate action if the claim validators fail. For example, if you have set the email verification recipe to be `"REQUIRED"`, and the user's email is not verified, this component will redirect the user to the email verification page. - -:::caution -At the moment the `SessionAuth` component does not support server side rendering and will only work on the client side. On the server side, this component renders an empty screen. - -Refer to the next section of this page to learn how to use sessions on the server side. -::: - -### Using `useSessionContext` - -```tsx title="app/components/homeClientComponent.tsx" -'use client' - -import { useSessionContext } from "supertokens-auth-react/recipe/session" - -export const HomeClientComponent = () => { - const session = useSessionContext(); - - if (session.loading) { - return
Loading...
; - } - - if (session.doesSessionExist === false) { - return
Session does not exist
; - } - - return ( -
-
-

- Client side component got userId: {session.userId}
-

-
-
- ); -} -``` - -`useSessionContext` lets you access the session information on the client side using the React Context API. `session.loading` indicates if the session is currently being loaded into the context, this will also refresh the session for you if it is expired. You can use `session.doesSessionExist` to check if a valid session exists and handle it accordingly. - -:::info -`useSessionContext` does not need to be used along with `SessionAuth`. Since our app is wrapped by the `SuperTokensWrapper` component, the `useSessionContext` hook can be used in any of our components. -::: - -:::tip Test by navigating to `/` -You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. -::: - -## Sessions with Server Components - -### Creating some helper Components - -#### Creating a wrapper around `SessionAuth` -Let's say we want to protect the home page of your website (`/` route). First we will create a wrapper around the `SessionAuth` component to add the `"use client"` directive on top. - -```tsx title="app/components/sessionAuthForNextJS.tsx" -"use client"; - -import React, { useState, useEffect } from "react"; -import { SessionAuth } from "supertokens-auth-react/recipe/session"; - -type Props = Parameters[0] & { - children?: React.ReactNode | undefined; -}; - -export const SessionAuthForNextJS = (props: Props) => { - const [loaded, setLoaded] = useState(false); - useEffect(() => { - setLoaded(true) - }, []) - if (!loaded) { - return props.children; - } - return {props.children}; -}; -``` - -This is a client component that renders just its children on the server side and renders the children wrapped with `SessionAuth` on the client side. This way, the server side returns the page content, and on the client, the same page content is wrapper with `SessionAuth` which will handle session related events on the frontend - for example, if the user's session expires whilst they are on this page, then `SessionAuth` will auto redirect them to the login page. - -#### Creating the `TryRefreshComponent` - -This component will refresh the user's session if their current session has expired. - -```tsx title="app/components/tryRefreshClientComponent.tsx" -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -import Session from "supertokens-auth-react/recipe/session"; -import SuperTokens from "supertokens-auth-react"; - -export const TryRefreshComponent = () => { - const router = useRouter(); - const [didError, setDidError] = useState(false); - - useEffect(() => { - /** - * `attemptRefreshingSession` will call the refresh token endpoint to try and - * refresh the session. This will throw an error if the session cannot be refreshed. - */ - void Session.attemptRefreshingSession() - .then((hasSession) => { - /** - * If the user has a valid session, we reload the page to restart the flow - * with valid session tokens - */ - if (hasSession) { - router.refresh(); - } else { - SuperTokens.redirectToAuth(); - } - }) - .catch(() => { - setDidError(true); - }); - }, [router]); - - /** - * We add this check to make sure we handle the case where the refresh API fails with - * an unexpected error - */ - if (didError) { - return
Something went wrong, please reload the page
; - } - - return
Loading...
; -}; -``` - -### Using `SessionAuthForNextJS` and checking for sessions - -We then create a server component that can check if the session exists and return any session information we may need: - -```tsx title="app/components/home.tsx" -import { cookies } from "next/headers"; -declare let SessionAuthForNextJS: any; // REMOVE_FROM_OUTPUT -import { redirect } from "next/navigation"; -// @ts-ignore -import { TryRefreshComponent } from "./tryRefreshClientComponent"; -// @ts-ignore -import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; -import jwksClient from "jwks-rsa"; -import JsonWebToken from "jsonwebtoken"; -import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; - -const client = jwksClient({ - jwksUri: "^{coreInfo.uri}/.well-known/jwks.json", -}); - -async function getAccessToken() { - const cookiesStore = await cookies(); - return cookiesStore.get("sAccessToken")?.value; -} - -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); -} - -/** - * A helper function to retrieve session details on the server side. - * - * NOTE: This function does not use the getSession / verifySession function from the supertokens-node SDK - * because those functions may update the access token. These updated tokens would not be - * propagated to the client side properly, as request interceptors do not run on the server side. - * So instead, we use regular JWT verification library - */ -async function getSSRSessionHelper(): Promise<{ - accessTokenPayload: JwtPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - const accessToken = await getAccessToken(); - const hasToken = !!accessToken; - try { - if (accessToken) { - const decoded = await verifyToken(accessToken); - return { accessTokenPayload: decoded, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } -} - -export async function HomePage() { - const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper(); - - if (error) { - return
Something went wrong while trying to get the session. Error - {error.message}
; - } - - // `accessTokenPayload` will be undefined if it the session does not exist or has expired - if (accessTokenPayload === undefined) { - if (!hasToken) { - /** - * This means that the user is not logged in. If you want to display some other UI in this - * case, you can do so here. - */ - return redirect("/auth"); - } - - /** - * This means that the session does not exist but we have session tokens for the user. In this case - * the `TryRefreshComponent` will try to refresh the session. - * - * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - */ - return ; - } - - /** - * SessionAuthForNextJS will handle proper redirection for the user based on the different session states. - * It will redirect to the login page if the session does not exist etc. - */ - return ( - -
- Your user id is: {accessTokenPayload.sub} -
-
- ); -} -``` - -The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. - -And then we can modify the `/app/page.tsx` file to use our server component - -```tsx title="app/page.tsx" -declare let HomePage: any; // REMOVE_FROM_OUTPUT -import styles from './page.module.css' -// @ts-ignore -import { HomePage } from "./components/home"; - -export default function Home() { - return ( -
- -
- ) -} -``` - -:::tip Test by navigating to `/` -You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. -::: - -:::important -An example of this can be seen [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/page.tsx). -::: - -
- - - -## Sessions with Client Components - -Checking for sessions in client components involves: - -- Using the `Session` recipe to manually check if a session exists, rendering some default UI while you check. -- Render your UI if a session exists. - -To learn more about how to do this refer to [this page](/docs/additional-verification/session-verification/protect-frontend-routes). - -## Sessions with Server Components - -### Creating a helper component for session refreshing - -Lets start by creating a component that will refresh the session if it exists and has expired. - -```tsx title="app/components/tryRefreshClientComponent.tsx" -"use client"; - -import { useEffect, useState } from "react"; -import { useRouter, redirect } from "next/navigation"; -import Session from "supertokens-web-js/recipe/session"; - -export const TryRefreshComponent = () => { - const router = useRouter(); - const [didError, setDidError] = useState(false); - - useEffect(() => { - void Session.attemptRefreshingSession() - .then((hasSession) => { - if (hasSession) { - router.refresh(); - } else { - /** - * This means that the session is expired and cannot be refreshed. - * In this example we redirect the user back to the login page. - */ - redirect("/auth"); - } - }) - .catch(() => { - setDidError(true); - }); - }, [router]); - - if (didError) { - return
Something went wrong, please reload the page
; - } - - return
Loading...
; -}; -``` - -`Session.attemptRefreshingSession` will call the refresh endpoint. `hasSession` will be: -- `true` if the session was refreshed -- `false` if the session could not be refreshed - -### Modify home page to check for sessions - -Lets modify the Home page server component we created earlier: - -```tsx title="app/components/home.tsx" -import { cookies } from "next/headers"; -declare let TryRefreshComponent: any; // REMOVE_FROM_OUTPUT -import { redirect } from "next/navigation"; -// @ts-ignore -import { TryRefreshComponent } from "./tryRefreshClientComponent"; -import jwksClient from "jwks-rsa"; -import JsonWebToken from "jsonwebtoken"; -import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; - -const client = jwksClient({ - jwksUri: "^{coreInfo.uri}/.well-known/jwks.json", -}); - -async function getAccessToken() { - const cookiesStore = await cookies(); - return cookiesStore.get("sAccessToken")?.value; -} - -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); -} - -/** - * A helper function to retrieve session details on the server side. - * - * NOTE: This function does not use the getSession / verifySession function from the supertokens-node SDK - * because those functions may update the access token. These updated tokens would not be - * propagated to the client side properly, as request interceptors do not run on the server side. - * So instead, we use regular JWT verification library - */ -async function getSSRSessionHelper(): Promise<{ - accessTokenPayload: JwtPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - const accessToken = await getAccessToken(); - const hasToken = !!accessToken; - try { - if (accessToken) { - const decoded = await verifyToken(accessToken); - return { accessTokenPayload: decoded, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } -} - -export async function HomePage() { - const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper(); - - if (error) { - return
Something went wrong while trying to get the session. Error - {error.message}
; - } - - // `accessTokenPayload` will be undefined if it the session does not exist or has expired - if (accessTokenPayload === undefined) { - if (!hasToken) { - /** - * This means that the user is not logged in. If you want to display some other UI in this - * case, you can do so here. - */ - return redirect("/auth"); - } - - /** - * This means that the session does not exist but we have session tokens for the user. In this case - * the `TryRefreshComponent` will try to refresh the session. - * - * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - */ - return ; - } - - return ( -
- Your user id is: {accessTokenPayload.sub} -
- ); -} -``` - -The `TryRefreshComponent` is a client component that checks if a session exists and tries to refresh the session if it is expired. - -And then we can modify the `/app/page.tsx` file to use our server component - -```tsx title="app/page.tsx" -declare let HomePage: any; // REMOVE_FROM_OUTPUT -import styles from './page.module.css' -// @ts-ignore -import { HomePage } from "./components/home"; - -export default function Home() { - return ( -
- -
- ) -} -``` - -:::tip Test by navigating to `/` -You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. - -For custom UI SuperTokens provides no login UI, the code above will redirect the user to the `/auth` route but you will have to build some UI that is served on that route. -::: - -
diff --git a/docs/quickstart/integrations/nextjs/app-directory/server-components-requests.mdx b/docs/quickstart/integrations/nextjs/app-directory/server-components-requests.mdx deleted file mode 100644 index 1ad63d0326..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/server-components-requests.mdx +++ /dev/null @@ -1,302 +0,0 @@ ---- -id: server-components-requests -title: 6. Making requests from Server Components -hide_title: true -sidebar_position: 7 -description: >- - Learn to make API requests from server components using access tokens in - Next.js. -page_type: tutorial -category: quickstart ---- - - -# 6. Making requests from Server Components - -Lets modify the Home page we made in a [previous step](/docs/quickstart/integrations/nextjs/app-directory/protecting-backend/session-verification-session-guard) to make a call to this API - - - - - -```tsx title="app/components/home.tsx" -import { cookies } from "next/headers"; -declare let TryRefreshComponent: any; // REMOVE_FROM_OUTPUT -declare let SessionAuthForNextJS: any; // REMOVE_FROM_OUTPUT -import { redirect } from "next/navigation"; -// @ts-ignore -import { TryRefreshComponent } from "./tryRefreshClientComponent"; -// @ts-ignore -import { SessionAuthForNextJS } from "./sessionAuthForNextJS"; -import jwksClient from "jwks-rsa"; -import JsonWebToken from "jsonwebtoken"; -import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; - -const client = jwksClient({ - jwksUri: "^{coreInfo.uri}/.well-known/jwks.json", -}); - -async function getAccessToken() { - const cookiesStore = await cookies(); - return cookiesStore.get("sAccessToken")?.value; -} - -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); -} - -/** - * A helper function to retrieve session details on the server side. - * - * NOTE: This function does not use the getSession / verifySession function from the supertokens-node SDK - * because those functions may update the access token. These updated tokens would not be - * propagated to the client side properly, as request interceptors do not run on the server side. - * So instead, we use regular JWT verification library - */ -async function getSSRSessionHelper(): Promise<{ - accessTokenPayload: JwtPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - const accessToken = await getAccessToken(); - const hasToken = !!accessToken; - try { - if (accessToken) { - const decoded = await verifyToken(accessToken); - return { accessTokenPayload: decoded, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } -} - -export async function HomePage() { - const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper(); - const accessToken = await getAccessToken(); - - if (error) { - return
Something went wrong while trying to get the session. Error - {error.message}
; - } - - // `accessTokenPayload` will be undefined if it the session does not exist or has expired - if (accessTokenPayload === undefined) { - if (!hasToken) { - /** - * This means that the user is not logged in. If you want to display some other UI in this - * case, you can do so here. - */ - return redirect("/auth"); - } - - /** - * This means that the session does not exist but we have session tokens for the user. In this case - * the `TryRefreshComponent` will try to refresh the session. - * - * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - */ - return ; - } - - // highlight-start - const userInfoResponse = await fetch('http://localhost:3000/api/user', { - headers: { - /** - * We read the access token from the cookies and use that as a Bearer token when - * making network requests. - */ - Authorization: 'Bearer ' + accessToken, - }, - }); - - let message = ""; - - if (userInfoResponse.status === 200) { - message = `Your user id is: ${accessTokenPayload.sub}` - } else if (userInfoResponse.status === 500) { - message = "Something went wrong" - } else if (userInfoResponse.status === 401) { - // The TryRefreshComponent will try to refresh the session - // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - return - } else if (userInfoResponse.status === 403) { - // SessionAuthForNextJS will redirect based on which claim is invalid - return ; - } - - // You can use `userInfoResponse` to read the users session information - // highlight-end - - return ( - -
- {message} -
-
- ); -} -``` - -We read the access token of the user from cookies. We can then send the access token as a header to the API. When the API calls `withSession` it will try to read the access token from the headers and if a session exists it will return the session information. - -
- - - -```tsx title="app/components/home.tsx" -import { cookies } from "next/headers"; -declare let TryRefreshComponent: any; // REMOVE_FROM_OUTPUT -import { redirect } from "next/navigation"; -// @ts-ignore -import { TryRefreshComponent } from "./tryRefreshClientComponent";import jwksClient from "jwks-rsa"; -import JsonWebToken from "jsonwebtoken"; -import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; - -const client = jwksClient({ - jwksUri: "^{coreInfo.uri}/.well-known/jwks.json", -}); - -async function getAccessToken() { - const cookiesStore = await cookies(); - return cookiesStore.get("sAccessToken")?.value; -} - -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); -} - -/** - * A helper function to retrieve session details on the server side. - * - * NOTE: This function does not use the getSession / verifySession function from the supertokens-node SDK - * because those functions may update the access token. These updated tokens would not be - * propagated to the client side properly, as request interceptors do not run on the server side. - * So instead, we use regular JWT verification library - */ -async function getSSRSessionHelper(): Promise<{ - accessTokenPayload: JwtPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - const accessToken = await getAccessToken(); - const hasToken = !!accessToken; - try { - if (accessToken) { - const decoded = await verifyToken(accessToken); - return { accessTokenPayload: decoded, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } -} - -export async function HomePage() { - const { accessTokenPayload, hasToken, error } = await getSSRSessionHelper(); - - if (error) { - return
Something went wrong while trying to get the session. Error - {error.message}
; - } - - // `accessTokenPayload` will be undefined if it the session does not exist or has expired - if (accessTokenPayload === undefined) { - if (!hasToken) { - /** - * This means that the user is not logged in. If you want to display some other UI in this - * case, you can do so here. - */ - return redirect("/auth"); - } - - /** - * This means that the session does not exist but we have session tokens for the user. In this case - * the `TryRefreshComponent` will try to refresh the session. - * - * To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - */ - return ; - } - - // highlight-start - const userInfoResponse = await fetch('http://localhost:3000/api/user', { - headers: { - /** - * We read the access token from the cookies and use that as a Bearer token when - * making network requests. - */ - Authorization: 'Bearer ' + getAccessToken(), - }, - }); - - let message = ""; - - if (userInfoResponse.status === 200) { - message = `Your user id is: ${accessTokenPayload.sub}` - } else if (userInfoResponse.status === 500) { - message = "Something went wrong" - } else if (userInfoResponse.status === 401) { - // The TryRefreshComponent will try to refresh the session - // To learn about why the 'key' attribute is required refer to: https://github.com/supertokens/supertokens-node/issues/826#issuecomment-2092144048 - return - } else if (userInfoResponse.status === 403) { - /** - * This means that one of the session claims is invalid. You should redirect the user to - * the appropriate page depending on which claim is invalid. - */ - return
Invalid Session Claims
; - } - - // You can use `userInfoResponse` to read the users session information - // highlight-end - - return ( -
- {message} -
- ); -} -``` - -APIs that require sessions will return status: -- `401` if there is no valid session or if the session has expired. In this case we return the `TryRefreshComponent` component which will try to refresh the session or redirect to the login page if the session cant be refreshed. -- `403` if one or more of the session claims fail their validation. In this case you should check which session claim failed and redirect the user accordingly. For example to check for the email verification claim you can refer to [this page](/docs/additional-verification/email-verification/protecting-routes). - -
diff --git a/docs/quickstart/integrations/nextjs/app-directory/setting-up-backend.mdx b/docs/quickstart/integrations/nextjs/app-directory/setting-up-backend.mdx deleted file mode 100644 index 15d5f9f114..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/setting-up-backend.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -id: setting-up-backend -title: 3. Adding auth APIs -hide_title: true -sidebar_position: 4 -description: >- - Add authentication APIs to your backend using SuperTokens for sign in and sign - up functionality. -page_type: tutorial -category: quickstart ---- - - -# 3. Adding auth APIs - -We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. - -## 1. Create the `app/api/auth/[[...path]]/route.ts` route -- Be sure to create the `auth/[[...path]]` folder in the `app/api/` folder. -- `route.ts` will use the `getAppDirRequestHandler` helper function exposed by `supertokens-node` which helps in calling all the APIs like sign in, sign up etc. (the full folder path should be `/app/api/auth/[[...path]]/route.ts`). -- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/api/auth/%5B...path%5D/route.ts). - -## 2. Expose the SuperTokens APIs - - - -```tsx title="app/api/auth/[[...path]]/route.ts" -declare let ensureSuperTokensInit: () => void; // REMOVE_FROM_OUTPUT -import { getAppDirRequestHandler } from 'supertokens-node/nextjs'; -import { NextRequest, NextResponse } from 'next/server'; -// @ts-ignore -import { ensureSuperTokensInit } from '../../../config/backend'; - -ensureSuperTokensInit(); - -const handleCall = getAppDirRequestHandler(); - -export async function GET(request: NextRequest) { - const res = await handleCall(request); - if (!res.headers.has('Cache-Control')) { - // This is needed for production deployments with Vercel - res.headers.set( - 'Cache-Control', - 'no-cache, no-store, max-age=0, must-revalidate' - ) - } - return res; -} - -export async function POST(request: NextRequest) { - return handleCall(request); -} - -export async function DELETE(request: NextRequest) { - return handleCall(request); -} - -export async function PUT(request: NextRequest) { - return handleCall(request); -} - -export async function PATCH(request: NextRequest) { - return handleCall(request); -} - -export async function HEAD(request: NextRequest) { - return handleCall(request); -} -``` - -:::note -In the snippet above we add the `Cache-Control` header to the responses for all auth APIs with the `GET` method. This is required if you are deploying your app with Vercel because API responses are automatically cached for production deployments. This results in problems because APIs such as `/session/refresh` return older session tokens resulting in infinite calls to refresh if an API returns unauthorised status. Setting the header ensures that Vercel does not cache any of the auth API responses. -::: - -## 3. Use the login widget -If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/docs/quickstart/integrations/nextjs/app-directory/setting-up-frontend.mdx b/docs/quickstart/integrations/nextjs/app-directory/setting-up-frontend.mdx deleted file mode 100644 index 4957ce7d17..0000000000 --- a/docs/quickstart/integrations/nextjs/app-directory/setting-up-frontend.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -id: setting-up-frontend -title: 2. Showing the Login UI -hide_title: true -sidebar_position: 3 -description: Implement a login UI using SuperTokens in a Next.js application. -page_type: tutorial -category: quickstart ---- - - -# 2. Showing the Login UI - - - - - -## 1. Create the `app/auth/[[...path]]/page.tsx` page -- Be sure to create the `auth/[[...path]]` folder in the `app` folder. -- `page.tsx` will contain the component for showing SuperTokens UI -- An example of this can be found [here](https://github.com/supertokens/next.js/blob/canary/examples/with-supertokens/app/auth/%5B%5B...path%5D%5D/page.tsx). - -## 2. Create the `Auth` component: - -```tsx title="app/auth/[[...path]]/page.tsx" -'use client'; - -import { useEffect, useState } from 'react'; -import { redirectToAuth } from 'supertokens-auth-react'; -import SuperTokens from 'supertokens-auth-react/ui'; -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; - -export default function Auth() { - // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. - const [loaded, setLoaded] = useState(false); - useEffect(() => { - if ( - SuperTokens.canHandleRoute([EmailPasswordPreBuiltUI]) === false - ) { - redirectToAuth({ redirectBack: false }); - } else { - setLoaded(true); - } - }, []); - - if (loaded) { - return SuperTokens.getRoutingComponent([EmailPasswordPreBuiltUI]); - } - - return null; -} -``` - -## 3. Visit `/auth` page on your website - -If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre-built UI, for each page on [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground). - -If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) - - - - - -You need to build your own UI. You will have to check each [authentication method tutorial](/docs/authentication/overview) for detailed instructions how how to achieve this. - - diff --git a/docs/quickstart/integrations/nextjs/pages-directory/_category_.json b/docs/quickstart/integrations/nextjs/pages-directory/_category_.json deleted file mode 100644 index 925eddf18b..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Pages Directory", - "position": 1 -} diff --git a/docs/quickstart/integrations/nextjs/pages-directory/about.mdx b/docs/quickstart/integrations/nextjs/pages-directory/about.mdx deleted file mode 100644 index a7920d578d..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/about.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: about -title: About -hide_title: true -sidebar_position: 1 -description: >- - Integrate SuperTokens with Next.js for authentication, session verification, - and route protection. -page_type: tutorial -category: quickstart ---- - - - - - - -# Overview steps - -Integrating SuperTokens with a Next.js app involves: -- Calling the frontend and backend init functions -- Adding a website page to display the auth related widgets (on `/auth` by default) -- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) -- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page -- Performing session verification: - - In your APIs - - In `getServerSideProps` - -## Try an example app -Download and run an example Next.js app quickly using the following command: - -```bash -npx create-supertokens-app@latest --frontend=next --recipe=emailpassword -``` - - - - - -# Overview steps - -Integrating SuperTokens with a Next.js app involves: -- Calling the frontend and backend init functions -- Building the various auth flows as per the [custom UI setup guide](../custom-ui/init/frontend). -- Creating a serverless function to expose the auth related APIs which will be consumed by the frontend widgets (on `/api/auth/` by default) -- Protecting website routes: Displaying them only when a user is logged in, else redirecting them to the login page -- Performing session verification: - - In your APIs - - In `getServerSideProps` - -## Try an example app - -Download and run an example Next.js app quickly using the following command: - -```bash -npx create-supertokens-app@latest --frontend=next --recipe=emailpassword -``` - -:::note -This example app uses our pre-built UI -::: - - diff --git a/docs/quickstart/integrations/nextjs/pages-directory/init.mdx b/docs/quickstart/integrations/nextjs/pages-directory/init.mdx deleted file mode 100644 index ce003c535e..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/init.mdx +++ /dev/null @@ -1,238 +0,0 @@ ---- -id: init -title: 1. Configuration -hide_title: true -sidebar_position: 2 -description: >- - Configure SuperTokens for authentication in your Next.js app with frontend and - backend setup. -page_type: tutorial -category: quickstart ---- - - -# 1. Configuration - - - - - -## 1. Install `supertokens` package -```bash -yarn add supertokens-node supertokens-auth-react supertokens-web-js nextjs-cors -``` - -## 2. Create configuration files -- Create a `config` folder in the root directory of your project -- Create an `appInfo.ts` inside the `config` folder. -- Create a `backendConfig.ts` inside the `config` folder. -- Create a `frontendConfig.ts` inside the `config` folder. - - -## 3. Create the `appInfo` configuration. - - - -```tsx title="/config/appInfo.ts" - -export const appInfo = { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - websiteDomain: "^{appInfo.websiteDomain}", - apiBasePath: "^{appInfo.apiBasePath}", - websiteBasePath: "^{appInfo.websiteBasePath}" -} - -``` - - - - - -## 1. Install `supertokens` package -```bash -yarn add supertokens-node supertokens-web-js nextjs-cors -``` - -## 2. Create configuration files -- Create a `config` folder in the root directory of your project -- Create an `appInfo.ts` inside the `config` folder. -- Create a `backendConfig.ts` inside the `config` folder. -- Create a `frontendConfig.ts` inside the `config` folder. - - -## 3. Create the `appInfo` configuration. - - - -```tsx title="/config/appInfo.ts" - -export const appInfo = { - // learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo - appName: "^{appInfo.appName}", - apiDomain: "^{appInfo.apiDomain}", - apiBasePath: "^{appInfo.apiBasePath}", -} - -``` - - - - - - -## 4. Create a frontend config function - -```tsx title="/config/frontendConfig.ts" -import EmailPasswordReact from 'supertokens-auth-react/recipe/emailpassword' -import SessionReact from 'supertokens-auth-react/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' -import Router from 'next/router' - -export const frontendConfig = () => { - return { - appInfo, - recipeList: [ - EmailPasswordReact.init(), - SessionReact.init(), - ], - windowHandler: (oI: any) => { - return { - ...oI, - location: { - ...oI.location, - setHref: (href: string) => { - Router.push(href) - }, - }, - } - }, - } -} -``` - - - - - -## 4. Create a frontend config function - -```tsx title="/config/frontendConfig.ts" -import EmailPasswordWebJs from 'supertokens-web-js/recipe/emailpassword' -import SessionWebJs from 'supertokens-web-js/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' - -export const frontendConfig = () => { - return { - appInfo, - recipeList: [ - EmailPasswordWebJs.init(), - SessionWebJs.init(), - ], - } -} -``` - - - -## 5. Create a backend config function - -```tsx title="/config/backendConfig.ts" showAppTypeSelect - -import EmailPasswordNode from 'supertokens-node/recipe/emailpassword' -import SessionNode from 'supertokens-node/recipe/session' -// @ts-ignore -import { appInfo } from './appInfo' -import { TypeInput } from "supertokens-node/types"; - -export const backendConfig = (): TypeInput => { - return { - framework: "express", - supertokens: { - connectionURI: "^{coreInfo.uri}", - apiKey: "^{coreInfo.key}", - }, - appInfo, - recipeList: [ - EmailPasswordNode.init(), - SessionNode.init(), - ], - isInServerlessEnv: true, - } -} - -``` - - - - -## 6. Call the frontend `init` functions and wrap with `` component - -- Create a `/pages/_app.tsx` file. You can learn more about this file [here](https://nextjs.org/docs/advanced-features/custom-app). - - -```tsx title="/pages/_app.tsx" -import '../styles/globals.css' -import React from 'react' -import { AppProps } from 'next/app' -import SuperTokensReact, { SuperTokensWrapper } from 'supertokens-auth-react' - -// @ts-ignore -import { frontendConfig } from '../config/frontendConfig' - -if (typeof window !== 'undefined') { - // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' - // highlight-next-line - SuperTokensReact.init(frontendConfig()) -} - -function MyApp({ Component, pageProps }: AppProps) { - return ( - // highlight-next-line - - - // highlight-next-line - - ); -} - -export default MyApp -``` - - - - - -## 7. Call the frontend `init` functions - -- Create a `/pages/_app.tsx` file. You can learn more about this file [here](https://nextjs.org/docs/advanced-features/custom-app). - - -```tsx title="/pages/_app.ts" -import '../styles/globals.css' -import React from 'react' -import { AppProps } from 'next/app' -import SuperTokensWebJs from 'supertokens-web-js' - -// @ts-ignore -import { frontendConfig } from '../config/frontendConfig' - -if (typeof window !== 'undefined') { - // we only want to call this init function on the frontend, so we check typeof window !== 'undefined' - // highlight-next-line - SuperTokensWebJs.init(frontendConfig()) -} - -function MyApp({ Component, pageProps }: AppProps) { - return ( - - ); -} - -export default MyApp -``` - - diff --git a/docs/quickstart/integrations/nextjs/pages-directory/next-steps.mdx b/docs/quickstart/integrations/nextjs/pages-directory/next-steps.mdx deleted file mode 100644 index e530063272..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/next-steps.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: next-steps -title: 6. Next steps -hide_title: true -sidebar_position: 7 -pagination_next: null -description: >- - Set up SuperTokens core and database for your app with managed or self-hosted - options. -page_type: tutorial -category: quickstart ---- - - -# 6. Next steps - -## Setting up the core and database - { - return ( - Are you using https://try.supertokens.com as the connection URI in the init function? - ) - }} defaultAnswer="Yes"> - - -You need to now setup an instance of the SuperTokens core for your app (that your backend should connect to). You have two options: -- [Managed service](/docs/quickstart/next-steps#configure-the-core-service) -- [Self hosted](/docs/deployment/self-host-supertokens) - - - - -:::success -You have successfully completed the quick setup! Head over to the "Post login operations" or "Common customizations" section. -::: - - - diff --git a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/_category_.json b/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/_category_.json deleted file mode 100644 index 65357b8f1b..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Checking Sessions in API Routes", - "position": 6 -} diff --git a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx b/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx deleted file mode 100644 index 1bdf9f5229..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-api.mdx +++ /dev/null @@ -1,82 +0,0 @@ ---- -id: in-api -title: 5a. Session verification in an API call -hide_title: true -sidebar_position: 1 -description: >- - Implement session verification in API calls to securely fetch user session - data. -page_type: tutorial -category: quickstart ---- - - -# 5a. Session verification in an API call - - - -:::note -This is applicable for when the frontend calls an API in the `/pages/api` folder. -::: - -For this guide, we will assume that we want an API `/api/user GET` which returns the current session information. - -## 1. Create a new file `/pages/api/user.ts` - -## 2. Call the `supertokens.init` function -Remember that whenever we want to use any functions from the `supertokens-node` lib, we have to call the `supertokens.init` function at the top of that serverless function file. - -```tsx title="pages/api/user.ts" -import supertokens from 'supertokens-node' -// @ts-ignore -import { backendConfig } from '../../../config/backendConfig' - -supertokens.init(backendConfig()) -``` - -## 3. Call the `verifySession` session function - - - -```tsx title="pages/api/user.ts" -import { superTokensNextWrapper } from 'supertokens-node/nextjs' -import { verifySession } from 'supertokens-node/recipe/session/framework/express' -import supertokens from 'supertokens-node' -// @ts-ignore -import { backendConfig } from '../../../config/backendConfig' -import NextCors from "nextjs-cors"; - -supertokens.init(backendConfig()) - -export default async function user(req: any, res: any) { - - // NOTE: We need CORS only if we are querying the APIs from a different origin - await NextCors(req, res, { - methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], - origin: "^{appInfo.websiteDomain}", - credentials: true, - allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - }); - - // we first verify the session - await superTokensNextWrapper( - async (next) => { - return await verifySession()(req, res, next) - }, - req, res - ) - // if it comes here, it means that the session verification was successful - - - return res.json({ - note: - 'Fetch any data from your application for authenticated user after using verifySession middleware', - userId: req.session.getUserId(), - sessionHandle: req.session.getHandle(), - userDataInAccessToken: req.session.getAccessTokenPayload(), - }) -} -``` - -- If no session exists, the API will return a `401` error to the client. In this case, the code `return res.json` will not be executed at all. -- In case the session does exist, `req.session` can be used to get session information. Learn more about this object [here](/docs/additional-verification/session-verification/protect-api-routes#using-verify-session). diff --git a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx b/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx deleted file mode 100644 index 548138b8c4..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/protecting-backend/in-ssr.mdx +++ /dev/null @@ -1,246 +0,0 @@ ---- -id: in-ssr -title: 5b. Session verification in getServerSideProps -hide_title: true -sidebar_position: 2 -description: >- - Verify user sessions in Next.js using `getServerSideProps` for secure route - access. -page_type: tutorial -category: quickstart ---- - - -# 5b. Session verification in getServerSideProps - - - -:::note -This is applicable for when verifying a session in `getServerSideProps` or `getInitialProps`. -::: - -For this guide, we will assume that we want to pass the logged in user's ID as a prop to a protected route. -::: - -```tsx -import jwksClient from "jwks-rsa"; -import JsonWebToken from "jsonwebtoken"; -import type { JwtHeader, JwtPayload, SigningKeyCallback } from "jsonwebtoken"; -import { GetServerSidePropsContext } from 'next'; - -const client = jwksClient({ - jwksUri: "^{coreInfo.uri}/.well-known/jwks.json", - async fetcher(jwksUri) { - return fetch(jwksUri).then((res) => res.json()); - }, -}); - -function getAccessToken(context: GetServerSidePropsContext ): string | undefined { - return context.req.cookies["sAccessToken"]; -} - -function getPublicKey(header: JwtHeader, callback: SigningKeyCallback) { - client.getSigningKey(header.kid, (err, key) => { - if (err) { - callback(err); - } else { - const signingKey = key?.getPublicKey(); - callback(null, signingKey); - } - }); -} - -async function verifyToken(token: string): Promise { - return new Promise((resolve, reject) => { - JsonWebToken.verify(token, getPublicKey, {}, (err, decoded) => { - if (err) { - reject(err); - } else { - resolve(decoded as JwtPayload); - } - }); - }); -} - -/** -* A helper function to retrieve session details on the server side. -* -* NOTE: This function does not use the getSession or verifySession functions from the supertokens-node SDK -* because they can update the access token. These updated tokens would not be -* propagated to the client side, as request interceptors do not run on the server side. -*/ -async function getSSRSessionHelper(context: GetServerSidePropsContext): Promise<{ - accessTokenPayload: JwtPayload | undefined; - hasToken: boolean; - error: Error | undefined; -}> { - const accessToken = getAccessToken(context); - const hasToken = !!accessToken; - try { - if (accessToken) { - const decoded = await verifyToken(accessToken); - return { accessTokenPayload: decoded, hasToken, error: undefined }; - } - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } catch (error) { - return { accessTokenPayload: undefined, hasToken, error: undefined }; - } -} - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const { accessTokenPayload, error } = await getSSRSessionHelper(context); - - if (error) { - throw error; - } - - if (accessTokenPayload === undefined) { - // This occurs if the token has expired or doesn't exist. - // Either way, sending this response prompts the frontend to attempt a session refresh. - // - // Case 1: Token doesn't exist - // - The refresh will fail, and the user will be redirected to the login page. - // - // Case 2: Token has expired - // - The client will call the refresh API and update the session tokens. - - return { props: { fromSupertokens: 'needs-refresh' } } - // or return {fromSupertokens: 'needs-refresh'} in case of getInitialProps - } - - return { - props: { userId: accessTokenPayload.sub } - } - - // or return { userId: accessTokenPayload.sub } in case of getInitialProps -} -``` - -:::caution -Don't use `getSession` or `verifySession` here. They might update the session tokens, and since our request interceptors don't run server-side, the updated token won't be propagated to the client side. -::: - -## 2. Doing manual refresh on the frontend - - - - - -- The following will refresh a session if needed, for all your website pages -- This goes in the `/pages/_app.tsx` file - -```tsx title="/pages/_app.tsx" -import React, { useEffect } from "react"; -import Session from 'supertokens-auth-react/recipe/session' -import { redirectToAuth } from 'supertokens-auth-react' -import { AppProps } from "next/app"; - -function MyApp({ Component, pageProps }: AppProps<{fromSupertokens: string}>) { - useEffect(() => { - - async function doRefresh() { - // pageProps.fromSupertokens === 'needs-refresh' will be true - // when in getServerSideProps, getSession throws a TRY_REFRESH_TOKEN - // error. - - if (pageProps.fromSupertokens === 'needs-refresh') { - if (await Session.attemptRefreshingSession()) { - // post session refreshing, we reload the page. This will - // send the new access token to the server, and then - // getServerSideProps will succeed - location.reload() - } else { - // the user's session has expired. So we redirect - // them to the login page - redirectToAuth() - } - } - } - doRefresh() - - }, [pageProps.fromSupertokens]) - - if (pageProps.fromSupertokens === 'needs-refresh') { - // in case the frontend needs to refresh, we show nothing. - // Alternatively, you can show a spinner. - - return null - } - - // the below is already there by default - return -} - -export default MyApp -``` - - - - - -```tsx title="/pages/_app.tsx" -import React, { useEffect } from "react"; -import Session from 'supertokens-web-js/recipe/session' -import { AppProps } from "next/app"; - -function MyApp({ Component, pageProps }: AppProps<{fromSupertokens: string}>) { - useEffect(() => { - - async function doRefresh() { - // pageProps.fromSupertokens === 'needs-refresh' will be true - // when in getServerSideProps, getSession throws a TRY_REFRESH_TOKEN - // error. - - if (pageProps.fromSupertokens === 'needs-refresh') { - if (await Session.attemptRefreshingSession()) { - // post session refreshing, we reload the page. This will - // send the new access token to the server, and then - // getServerSideProps will succeed - location.reload() - } else { - // the user's session has expired. So we redirect - // them to the login page - - // redirect to login page - window.location.href = "/login" - } - } - } - doRefresh() - - }, [pageProps.fromSupertokens]) - - if (pageProps.fromSupertokens === 'needs-refresh') { - // in case the frontend needs to refresh, we show nothing. - // Alternatively, you can show a spinner. - - return null - } - - // the below is already there by default - return -} - -export default MyApp -``` - - - -## 3. Consume the `userId` returned by getServerSideProps in your component - -On success, `getServerSideProps` returns -```tsx -{ - // Refer to Step 1) - // @ts-ignore - props: { userId: accessTokenPayload.sub } -} -``` - -Therefore, the associated page can access the `userId` like: - -```tsx -export default function Home(props: any) { - let userId = props.userId; -} -``` diff --git a/docs/quickstart/integrations/nextjs/pages-directory/protecting-route.mdx b/docs/quickstart/integrations/nextjs/pages-directory/protecting-route.mdx deleted file mode 100644 index 094a7a9757..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/protecting-route.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -id: protecting-route -title: 4. Protecting a website route -hide_title: true -sidebar_position: 5 -description: >- - Protect website routes by requiring user authentication and redirecting - unauthenticated users to login. -page_type: tutorial -category: quickstart ---- - -import { OAuthFrontendVerificationCallout } from "/src/components/Callouts"; - -# 4. Protecting a website route - - - - - - - -Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page. - -Let's say we want to protect the home page of your website (`/` route). In this case, we can edit the `/pages/index.tsx` file to add an auth wrapper around your `Home` component like so: - -```tsx title="pages/index.tsx" -import React from 'react' -import dynamic from 'next/dynamic' -import { SessionAuth } from 'supertokens-auth-react/recipe/session' -// @ts-ignore -import ProtectedPage from "./protectedPage"; - -export default function Home() { - return ( - // we protect ProtectedPage by wrapping it with SessionAuth - - - - ) -} -``` - -:::tip Test by navigating to `/` -You should be redirected to the login page. After that, sign in, and then visit `/` again. This time, there should be no redirection. -::: - - - - - -Protecting a website route means that it cannot be accessed unless a user is signed in. If a non signed in user tries to access it, they will be redirected to the login page. - -You can do this by using the `doesSessionExist` function as shown below - -```tsx title="pages/index.tsx" -import Session from 'supertokens-web-js/recipe/session'; - -async function doesSessionExist() { - if (await Session.doesSessionExist()) { - // user is logged in - } else { - // user has not logged in yet - } -} -``` - - diff --git a/docs/quickstart/integrations/nextjs/pages-directory/setting-up-backend.mdx b/docs/quickstart/integrations/nextjs/pages-directory/setting-up-backend.mdx deleted file mode 100644 index afea60506c..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/setting-up-backend.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -id: setting-up-backend -title: 3. Adding auth APIs -hide_title: true -sidebar_position: 4 -description: >- - Add authentication APIs to your backend using SuperTokens for user sign-in and - sign-up. -page_type: tutorial -category: quickstart ---- - - -# 3. Adding auth APIs - -We will add all the backend APIs for auth on `/api/auth`. This can be changed by setting the `apiBasePath` property in the `appInfo` object in the `appInfo.ts` file. For the rest of this page, we will assume you are using `/api/auth`. - -## 1. Create the `pages/api/auth/[[...path]].tsx` page -- Be sure to create the `auth` folder in the `pages/api/` folder. -- `[[...path]].tsx` will use the middleware exposed by `supertokens-node` which exposes all the APIs like sign in, sign up etc.. - -## 2. Expose the SuperTokens APIs - - - -```tsx title="pages/api/auth/[[...path]].ts" -import { superTokensNextWrapper } from 'supertokens-node/nextjs' -import { middleware } from 'supertokens-node/framework/express' -import { NextApiRequest, NextApiResponse } from 'next' -import { Request, Response } from 'express'; -import supertokens from 'supertokens-node' -// @ts-ignore -import { backendConfig } from '../../../config/backendConfig' -import NextCors from "nextjs-cors"; - -supertokens.init(backendConfig()) - -export default async function superTokens( - req: NextApiRequest & Request, - res: NextApiResponse & Response -) { - - await NextCors(req, res, { - methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"], - origin: "^{appInfo.websiteDomain}", - credentials: true, - allowedHeaders: ["content-type", ...supertokens.getAllCORSHeaders()], - }); - - await superTokensNextWrapper( - async (next) => { - res.setHeader( - "Cache-Control", - "no-cache, no-store, max-age=0, must-revalidate" - ); - await middleware()(req, res, next) - }, - req, - res - ) - if (!res.writableEnded) { - res.status(404).send('Not found') - } -} -``` - -:::note -In the snippet above we add the `Cache-Control` header to the responses for all auth APIs. This is required if you are deploying your app with Vercel because API responses are automatically cached for production deployments. This results in problems because APIs such as `/session/refresh` return older session tokens resulting in infinite calls to refresh if an API returns unauthorised status. Setting the header ensures that Vercel does not cache any of the auth API responses. -::: - -## 3. Use the login widget -If you are now able to sign in or sign up, this means the backend setup is done correctly! If not, please feel free to ask questions on [Discord](https://supertokens.com/discord) diff --git a/docs/quickstart/integrations/nextjs/pages-directory/setting-up-frontend.mdx b/docs/quickstart/integrations/nextjs/pages-directory/setting-up-frontend.mdx deleted file mode 100644 index b1e00792d9..0000000000 --- a/docs/quickstart/integrations/nextjs/pages-directory/setting-up-frontend.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -id: setting-up-frontend -title: 2. Showing Login UI -hide_title: true -sidebar_position: 3 -description: Implement a login UI using SuperTokens in a Next.js application. -page_type: tutorial -category: quickstart ---- - - -# 2. Showing the Login UI - - - - - -## 1. Create the `pages/auth/[[...path]].tsx` page -- Be sure to create the `auth` folder in the `pages` folder. -- `[[...path]].tsx` will contain the component for showing SuperTokens UI - -## 2. Create the `Auth` component: - -```tsx title="pages/auth/[[...path]].tsx" -import React, { useEffect } from 'react' -import dynamic from 'next/dynamic' -import { EmailPasswordPreBuiltUI } from 'supertokens-auth-react/recipe/emailpassword/prebuiltui'; -import { redirectToAuth } from 'supertokens-auth-react' -import { canHandleRoute, getRoutingComponent } from 'supertokens-auth-react/ui' - -const SuperTokensComponentNoSSR = dynamic<{}>( - new Promise((res) => res(() => getRoutingComponent([EmailPasswordPreBuiltUI]))), - { ssr: false } -) - -export default function Auth() { - - // if the user visits a page that is not handled by us (like /auth/random), then we redirect them back to the auth page. - useEffect(() => { - if (canHandleRoute([EmailPasswordPreBuiltUI]) === false) { - redirectToAuth() - } - }, []) - - return ( - - ) -} -``` - -## 3. Visit `/auth` page on your website - -If you see a login UI, then you have successfully completed this step! You can also see all designs of our pre-built UI, for each page on [this link](https://master--6571be2867f75556541fde98.chromatic.com/?path=/story/auth-page--playground). - -If you cannot see the UI in your app, please feel free to ask questions on [Discord](https://supertokens.com/discord) - - - - - -You need to build your own UI. You will have to check each [authentication method tutorial](/docs/authentication/overview) for detailed instructions how how to achieve this. - - diff --git a/docs/quickstart/integrations/overview.mdx b/docs/quickstart/integrations/overview.mdx deleted file mode 100644 index 8502eea13d..0000000000 --- a/docs/quickstart/integrations/overview.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Overview -sidebar_position: 1 -pagination_next: null -pagination_prev: null -hide_table_of_contents: true -skip_llms_txt: true -description: >- - Explore different integration guides that present how to use SuperTokens with - different platforms and frameworks. -page_type: tutorial -category: quickstart ---- - - - -# Overview - - - Explore different integration guides that present how to use SuperTokens with different platforms and frameworks. - - - - - - -## Frameworks - - - - - - Next.js App Router - - - - - Next.js Pages Router - - - - - - NestJS - - - - - GraphQL - - - - - - RedwoodJS - - - - - Capacitor - - - - - -## Cloud Platforms - - - - - - AWS Lambda - - - - - Netlify - - - - - - Vercel - - - - - Hasura - - - - - - - Supabase - - - - - diff --git a/docs/quickstart/integrations/supabase.mdx b/docs/quickstart/integrations/supabase.mdx deleted file mode 100644 index 750af661ce..0000000000 --- a/docs/quickstart/integrations/supabase.mdx +++ /dev/null @@ -1,443 +0,0 @@ ---- -title: Supabase -hide_title: true -sidebar_position: 8 -description: >- - Integrate SuperTokens with Supabase in a Next.js app for secure user data - management. -page_type: tutorial -category: quickstart ---- - - -# Supabase Guide - -## Overview - -The following guide shows you how to integrate a Next.js app with SuperTokens and Supabase. -It includes instructions on how to: -- Create a Supabase project with a table to store your user data -- Create a Supabase JWT and store the user's session -- Enable row level security policies in your Supabase table to ensure only authorized users can access their data - -In this example, the user's email is stored mapped to their SuperTokens userId in Supabase. - -You can also check an [example repository](https://github.com/supertokens/supertokens-auth-react/tree/master/examples/with-supabase) for specific references. - -## Before you start - -The guide does not include instructions on how to setup a Next.js app with SuperTokens. -To do this you can follow the [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) instructions. - -## Steps - -### 1. Configure Supabase - -Supabase provides a database with authentication and authorization features. This guide uses Supabase to store the user's info mapped to their SuperTokens `userId`. - -#### 1.1 Create a new Supabase project - -1. From your [Supabase dashboard](https://app.supabase.com/), click New project. -2. Enter a Name for your Supabase project. -3. Enter a secure Database Password. -4. Select the same Region you host your app's backend in. -5. Click Create new project. - -![Supabase dashboard](/img/thirdpartyemailpassword/supabase/supabase_dashboard_create.png) - -#### 1.2 Create the user table in Supabase - -1. From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), click Table editor, then New table. -2. Enter `users` as the `Name` field. -3. Select `Enable Row Level Security (RLS).` -4. Remove the default columns -5. Create two new columns: - - `user_id` as `varchar` as primary key - - email as `varchar` -6. Click `Save` to create the new table. - -![Supabase table create](/img/thirdpartyemailpassword/supabase/supabase_table_create.png) - -### 2. Setup JWT creation - -In this section, the SuperTokens backend is overridden to create a JWT signed with Supabase's secret which contains the user's `userId`. -This token is used on the frontend and backend to read and write to Supabase's database. - -#### 2.1 Integrate your Next.js app with SuperTokens - -Follow either the [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) guides for instructions on how to configure your application. - -#### 2.2 Save the Supabase configuration values - -Retrieve the Supabase configuration values from the dashboard and add them to your `.env` file: - -```bash -// retrieve the following from your supabase dashboard - -NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_KEY= -SUPABASE_SIGNING_SECRET= -``` - -#### 2.3 Create the Supabase JWT - -In the Next.js app when a user signs up, you'll want to store the user's email in Supabase. -The email can then be retrieved from Supabase and displayed on the frontend. -To use the Supabase client to query the database, you need to create a JWT signed with your Supabase app's signing secret. -This JWT also needs to contain the user's `userId` so Supabase knows an authorized user is making the request. -To create this flow, SuperTokens needs to be modified so that, when a user signs up or signs in, a JWT signed with Supabase's signing secret is created and attached to the user's session. -Attaching the JWT to the user's session allows the Supabase JWT to be retrieved on the frontend and backend (post session verification), which can then be used to query Supabase. - -To create the JWT signed with Supabase's signing secret, the `jsonwebtoken` library is used. - -```bash -npm install jsonwebtoken -``` - -The JWT can be added to the user's session by overriding the `createNewSession` function and adding it to the `accessTokenPayload` - -```ts -// config/backendConfig.ts - -import EmailPassword from "supertokens-node/recipe/emailpassword"; -import SessionNode from "supertokens-node/recipe/session"; -import { TypeInput, AppInfo } from "supertokens-node/types"; -import jwt from "jsonwebtoken"; - -let appInfo: AppInfo = { - appName: "TODO: add your app name", - apiDomain: "TODO: add your website domain", - websiteDomain: "TODO: add your website domain" -} - -let supabase_signing_secret = process.env.SUPABASE_SIGNING_SECRET || "TODO: Your Supabase Signing Secret"; - -let backendConfig = (): TypeInput => { - return { - framework: "express", - supertokens: { - connectionURI: "https://try.supertokens.com", - }, - appInfo, - recipeList: [ - // @ts-ignore - EmailPassword.init({/*...*/}), - SessionNode.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - createNewSession: async function (input) { - const payload = { - userId: input.userId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - }; - - const supabase_jwt_token = jwt.sign(payload, supabase_signing_secret); - - input.accessTokenPayload = { - ...input.accessTokenPayload, - supabase_token: supabase_jwt_token, - }; - - return await originalImplementation.createNewSession(input); - }, - }; - }, - }, - }), - ], - isInServerlessEnv: true, - }; -}; -``` - -### 3. Create a Supabase client - -A client is created to interact with Supabase using the `supabase-js` library. - -#### 3.1 Install the `supabase-js` library - - ```bash - npm install @supabase/supabase-js - ``` - -#### 3.2 Create a new file called `utils/supabase.ts` and add the following: - - ```ts - // utils/supabase.ts - - import { createClient } from '@supabase/supabase-js' - - let supabase_url = process.env.NEXT_PUBLIC_SUPABASE_URL || "TODO: Your Supabase URL" - let supabase_key = process.env.NEXT_PUBLIC_SUPABASE_KEY || "TODO: Your Supabase Key" - - const getSupabase = (access_token: string) => { - const supabase = createClient( - supabase_url, - supabase_key - ) - - supabase.auth.session = () => ({ - access_token, - token_type: "", - user: null - }) - - return supabase - } - - export { getSupabase } - ``` - -### 4. Insert users into Supabase when they sign up - -In this example app, the user can sign up via Email-Password authentication. -The API needs to be overridden such that when a user signs up, their email mapped to their userId is stored in Supabase. - -#### 4.1 Override the Email-Password sign up function - -```ts -// config/backendConfig.ts - -let appInfo: AppInfo = { - appName: "TODO: add your app name", - apiDomain: "TODO: add your website domain", - websiteDomain: "TODO: add your website domain" -} - -// take a look at the Creating Supabase Client section to see how to define getSupabase -let getSupabase: any; - -let backendConfig = (): TypeInput => { - return { - framework: "express", - supertokens: { - connectionURI: "https://try.supertokens.com", - }, - appInfo, - recipeList: [ - ^{recipeNameCapitalLetters}.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - // the signUpPOST function handles sign up - signUpPOST: async function (input) { - if (originalImplementation.signUpPOST === undefined) { - throw Error("Should never come here"); - } - - let response = await originalImplementation.signUpPOST(input); - - if (response.status === "OK" && response.user.loginMethods.length === 1 && input.session === undefined) { - - // retrieve the accessTokenPayload from the user's session - const accessTokenPayload = response.session.getAccessTokenPayload(); - - // create a supabase client with the supabase_token from the accessTokenPayload - const supabase = getSupabase(accessTokenPayload.supabase_token); - - // store the user's email mapped to their userId in Supabase - const { error } = await supabase - .from("users") - .insert({ email: response.user.emails[0], user_id: response.user.id }); - - if (error !== null) { - - throw error; - } - } - - return response; - }, - }; - }, - }, - }), - SessionNode.init({/*...*/}), - ], - isInServerlessEnv: true, - }; -}; - -``` - -The Email-Password sign up flow is changed by overriding the `signUpPOST` API. -When a user signs up, the `supabase_token` is retrieved from the user's `accessTokenPayload`(this was added in the previous step where the `createNewSession` function was changed) and used to query Supabase to insert the new user's information. - - -### 5. Retrieve the user email on the frontend - - - - - -With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. - -```tsx -// pages/index.tsx - -import React, { useState, useEffect } from 'react' -import Head from 'next/head' -import { SessionAuth, useSessionContext } from 'supertokens-auth-react/recipe/session' - -// take a look at the Creating Supabase Client section to see how to define getSupabase -let getSupabase: any; - -export default function Home() { - return ( - // The ProtectedPage component is wrapped with the SessionAuth so only an - // authenticated user can access it. - - - - ) -} - -function ProtectedPage() { - // retrieve the authenticated user's accessTokenPayload and userId from the sessionContext - const session = useSessionContext() - - const [userEmail, setEmail] = useState('') - useEffect(() => { - async function getUserEmail() { - if (session.loading) { - return; - } - // retrieve the supabase client who's JWT contains users userId, this is - // used by supabase to check that the user can only access table entries which contain their own userId - const supabase = getSupabase(session.accessTokenPayload.supabase_token) - - // retrieve the user's name from the users table whose email matches the email in the JWT - const { data } = await supabase.from('users').select('email').eq('user_id', session.userId) - - if (data.length > 0) { - setEmail(data[0].email) - } - } - getUserEmail() - }, [session]) - - if (session.loading) { - return null; - } - - return ( -
- - SuperTokens 💫 - - - -
-

- You are authenticated with SuperTokens! (UserId: {session.userId}) -
- Your email retrieved from Supabase: {userEmail} -

-
-
- ) -} -``` - -
- - - -With the backend setup, the frontend can be modified to retrieve the user's email from Supabase. - -```tsx -import Session from "supertokens-web-js/recipe/session"; - -// take a look at the Creating Supabase Client section to see how to define getSupabase -let getSupabase: any; - -async function getEmailFromSupabase() { - if (await Session.doesSessionExist()) { - let accessTokenPayload = await Session.getAccessTokenPayloadSecurely(); - const supabase = getSupabase(accessTokenPayload.supabase_token) - - const { data } = await supabase.from('users').select('email').eq('user_id', await Session.getUserId()) - - if (data.length > 0) { - return data[0].email; - } - return undefined; - } - throw new Error("Session does not exist"); -} -``` - - - -As seen above, the access token payload is fetched from SuperTokens to retrieve the authenticated user's Supabase access token which can be used to fetch the user's email from Supabase. - -### 6. Enforce row level security for select and insert requests - -To enforce Row Level Security for the Users table, you need to create policies for Select and Insert requests. -These polices retrieve the `userId` from the JWT and check if it matches the `userId` in the Supabase table. -A PostgreSQL function is needed to extract the `userId` from the JWT. - -The payload in the JWT has the following structure: - -```bash -{ - userId, - exp -} -``` - -#### 6.1 Create PostgreSQL function to retrieve `userId` from JWT - -To create the PostgreSQL function, navigate back to the Supabase dashboard, select `SQL` from the sidebar menu, and click `New query`. This creates a new query called `new sql snippet`, which allows you to run any SQL against the Postgres database. -Write the following and click `Run`. - - ```bash - create or replace function auth.user_id() returns text as $$ - select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text; - $$ language sql stable; - ``` - -- This creates a function called `auth.user_id()`, which inspects the `userId` field of our JWT payload. - -#### 6.2 Create Policies for `SELECT` and `INSERT` queries: - -##### `SELECT` query policy - -The first policy checks whether the user is the owner of the email being retrieved. - -- Select `Authentication` from the Supabase sidebar menu, click `Policies`, and then `New Policy` on the `Users` table. - - ![Create policy](/img/thirdpartyemailpassword/supabase/create_policy.png) - -- From the modal, select `Create a policy from scratch` and add the following. - - ![select policy](/img/thirdpartyemailpassword/supabase/policy_config_select.png) - -- This policy is calling the PostgreSQL function we just created to get the currently logged in user's ID `auth.user_id()` and checking whether this matches the `user_id` column for the current `email`. If it does, then it allows the user to select it, otherwise it continues to deny. - -- Click `Review` and then `Save policy`. - -##### `INSERT` query policy - -The second policy checks whether the `user_id` being inserted is the same as the `userId` in the JWT. - -- Create another policy and add the following: - - ![insert policy](/img/thirdpartyemailpassword/supabase/policy_config_insert.png) - -Similar to the previous policy, the PostgreSQL function that was created is called to get the currently logged in user's ID and check whether this matches the `user_id` column for the row being inserted. If it does, then it allows the user to insert the row, otherwise it continues to deny. - -Click `Review` and then `Save policy`. - -### 6.3 Test your changes - -You can now sign up and you should see the following screen: - -![auth screen](/img/thirdpartyemailpassword/supabase/supabase_app_authenticated_screen.png) - -If you navigate to your table you should see a new row with the user's `user_id` and `email`. - -![table with user](/img/thirdpartyemailpassword/supabase/table_with_user.png) diff --git a/docs/quickstart/integrations/vercel.mdx b/docs/quickstart/integrations/vercel.mdx deleted file mode 100644 index 4f749d4688..0000000000 --- a/docs/quickstart/integrations/vercel.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Vercel -hide_title: true -show_ui_switcher: true -description: 'Learn how to integrate SuperTokens with Vercel ' -page_type: tutorial -category: quickstart ---- - - -# Vercel Guide - - -This page only talks about what environment variables to use when you are deploying an application on Vercel. -For a full set of instructions on how to integrate **SuperTokens** in a **Next.js** project, please see either our [app router](/docs/quickstart/integrations/nextjs/app-directory/about) or [pages router](/docs/quickstart/integrations/nextjs/app-directory/about) guides. - - -## Working with Vercel's inspect and production URL - -Vercel provides one production URL per app and one unique inspect URL per deployment. -To get SuperTokens to work with dynamic URLs, you need to make the following changes to the [`appInfo` object](/docs/references/frontend-sdks/reference#sdk-configuration): - -### On the frontend - - - - - - -```text -appInfo = { - apiDomain: window.location.origin, - websiteDomain: window.location.origin, - ... -} -``` - - - - - - - -```text appInfo = { - apiDomain: window.location.origin - ... -} -``` - - -### On the backend - -```text -appInfo = { - apiDomain: process.env.VERCEL_URL, - websiteDomain: process.env.VERCEL_URL, - ... -}, -``` - -Vercel adds an environment variable to the backend - `VERCEL_URL`, which points to the current URL that the app is deployed on. This allows SuperTokens to work on all inspect URLs generated by Vercel without you having to keep changing your code. - -:::note -The above setting works only if your backend and frontend are deployed on the same URL. If you are using a different backend and using Vercel only for your frontend, then: -- Set the `apiDomain` on the frontend and backend to point to your backend. -- The `websiteDomain` on the frontend should be `window.location.origin`, but on the backend, it should be equal to your production deployment URL. This will break certain features of the app for inspect URL deployments, but it will work as expected for production deployments. -::: - - diff --git a/docs/quickstart/introduction.mdx b/docs/quickstart/introduction.mdx deleted file mode 100644 index a265bcf643..0000000000 --- a/docs/quickstart/introduction.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -id: introduction -title: Introduction -hide_title: true -sidebar_position: 1 -description: >- - Set up user authentication with SuperTokens using Email/Password login and - customizable UI components. -page_type: tutorial -category: quickstart ---- - -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -# Introduction - -## Overview - -This quickstart will guide you through how to set up a basic project that uses **SuperTokens** to authenticate users. -The tutorial shows an **Email/Password** login flow, rendered by either our **Prebuilt UI components** or by your own **Custom UI**. - - -If you want to skip straight to an example application you can choose between: - -- Checking our live [demo application](https://emailpassword.demo.supertokens.com/auth) -- Running a **SuperTokens** project from your local machine. You just have to use our CLI app and execute the following command: - -```bash -npx create-supertokens-app@latest --recipe=emailpassword -``` - - -## Before you start - -Before going into the actual tutorial, let's get a clear picture of how **SuperTokens** works and some of the terms that we will use throughout the documentation. - -### SuperTokens Core - -The main service that provides all the functionality is called the **SuperTokens Core**. SDKs communicate over an API with this service in order to -perform authentication related tasks. - -Unlike with other providers, the **SuperTokens Frontend SDK** never talks to the **Authentication Service** directly. -All the requests target your existing **Backend Service**. -From there, the **Backend SDKs** are used to expose new authentication routes. Those in turn communicate with the **SuperTokens Core**. - -You can check the following diagram for a high level overview of how the services will interact within an authentication setup that involves **SuperTokens**. - - - - Flowchart of architecture when using SuperTokens managed service - - - Flowchart of architecture when self-hosting SuperTokens - - - -:::info Edge Cases - -- You can also host the **SuperTokens Core** yourself. In that case your backend will communicate with a service that exists inside your infrastructure. -- If you are using a backend for which we do not have an SDK, you will have to spin up an additional auth service in a language for which we do have a backend SDK (NodeJS, Python or Golang). -- The **SuperTokens Core** is a trusted backend component. It should only be reachable by your backend, never exposed directly to the public internet or your frontend. When self-hosting, see [Secure the core](/docs/deployment/self-host-supertokens#secure-the-core). - ::: - -### Recipes - -The functionalities that **SuperTokens** provides are bundled into objects that can be referred to as **Recipes**. -Everything from _authentication methods_ to _session and user management_ can be included under this concept. -In the following sections, we will see how recipes get initialised and configured and how you can customise them to fit your use case. - -Now that we have cleared all this out, we can move forward with the actual tutorial. -Go to the next page to see how to configure your [Frontend Application](./frontend-setup). diff --git a/docs/quickstart/next-steps.mdx b/docs/quickstart/next-steps.mdx deleted file mode 100644 index 5f7df74ef6..0000000000 --- a/docs/quickstart/next-steps.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -id: next-steps -title: Next Steps -hide_title: true -pagination_next: null -sidebar_position: 4 -description: >- - Configure SuperTokens for production-ready authentication, explore additional - methods, and customize flows. -page_type: tutorial -category: quickstart ---- - - -# Next Steps - -## Overview - -Now that you have completed the quickstart guide there are a few more things that you need to take care of on the road towards a production ready authentication experience. - -## Configure the Core Service - -If you have signed up and deployed a SuperTokens environment already, you can skip this step. -Otherwise, please follow these instructions to use the correct **SuperTokens Core** instance in your application. - -The steps show you how to connect to a **SuperTokens Managed Service Environment**. -If you want to self host the core instance please check the [following guide](/docs/deployment/self-host-supertokens). - - -### 1. Sign up for a SuperTokens account - -Open this [page](https://supertokens.com/auth) in order to access the account creation page. -Select the account that you want to use and wait for the action to complete. - -### 2. Create a deployment - -Integration with SuperTokens SDKs - -After you have signed up, select **Managed** and create a deployment. -Choose a region that is closest to where your services are hosted, enter a deployment name, and click **Deploy Core**. - -Our internal service will deploy a separate environment based on your selection. -After this process is complete, open the new deployment from the list. - -:::info - -The initial setup flow only configures a development environment. -In order to use SuperTokens in production, you will have to create a separate deployment. - -::: - -### 3. Connect the Backend SDK with SuperTokens 🔌 - -SuperTokens managed service dashboard connectionURI and API key - -Open the newly created deployment and stay on its **Overview** page. In the **Connection Information** section, copy the `Connection URI` and an `API Key` and paste them in your backend code. You can use **Generate Key** to create an additional API key. - - - - - - -```tsx -import supertokens from "supertokens-node"; - -supertokens.init({ - // highlight-start - supertokens: { - connectionURI: "", - apiKey: "", - }, - // highlight-end - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [], -}); -``` - - - - -```go -import "github.com/supertokens/supertokens-golang/supertokens" - -func main() { - supertokens.Init(supertokens.TypeInput{ - // highlight-start - Supertokens: &supertokens.ConnectionInfo{ - ConnectionURI: "", - APIKey: "", - }, - // highlight-end - }) -} - -``` - - - - -```python -from supertokens_python import init, InputAppInfo, SupertokensConfig - -init( - app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - # highlight-start - supertokens_config=SupertokensConfig( - connection_uri='', - api_key='' - ), - # highlight-end - framework='...', # type: ignore - recipe_list=[ - #... - ] -) -``` - - - - -## Add More Authentication Methods - -Besides the simple email/password example that you just completed, **SuperTokens** exposes additional authentication methods that can easily be enabled. - - - - - Passwordless - - - Authentication through magic links or one-time codes. - - - - - Social Login - - - Login flow that uses third-party providers for authentication. - - - - - Enterprise Login - - - Instructions on how to configure your application to support multiple tenants and enterprise authentication methods. - - - - - Unified Login - - - Details on how to create a common authentication experience for all your products. - - - - - Machine to Machine Authentication - - - Guides on authenticating microservices using SuperTokens. - - - - - -## Customize Your Authentication Flow - -After you have connected the Backend SDK to a specific **SuperTokens Core Service**, you can go check the rest of the documentation. -Several sections show you how to customize the authentication experience to fit your specific needs. -Some of the most common subjects are: - - - - Add Email Verification - Use an additional verification step to secure your sign up process. - - - Add a Custom Redirect Action - Redirect users after sign up/login. - - - Use Custom Session Management - Adjust the session management to fit your needs. - - - Share Sessions Across Subdomains - Manage multiple subdomains from a single SuperTokens Core instance. - - - Post Sign In Actions - Add custom logic in the authentication flow. - - - -## Explore Additional Features - -You can also review the additional features that **SuperTokens** exposes. -Those can help you extend the authentication implementation on different levels. - - - - Self Host SuperTokens Core - Run the SuperTokens in your own infrastructure. - - - Migration Guide - Migrate your data from other authentication providers. - - - Multi Factor Authentication - Set up additional authentication layers in your sign-in process. - - - User Management Dashboard - Administrate your users from the SuperTokens Dashboard. - - - User Roles and Permissions - Adjust the authorization settings in your application. - - diff --git a/docs/references/_category_.json b/docs/references/_category_.json deleted file mode 100644 index 299f1d0553..0000000000 --- a/docs/references/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "References", - "customProps": { - "isMainCategory": true - }, - "position": 1 -} diff --git a/docs/references/backend-sdks/_category_.json b/docs/references/backend-sdks/_category_.json deleted file mode 100644 index 0800d5ddbc..0000000000 --- a/docs/references/backend-sdks/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Backend SDKs", - "customProps": { - "isMainCategory": true - }, - "position": 4 -} diff --git a/docs/references/backend-sdks/api-overrides.mdx b/docs/references/backend-sdks/api-overrides.mdx index fb8b9aca78..0e0690438d 100644 --- a/docs/references/backend-sdks/api-overrides.mdx +++ b/docs/references/backend-sdks/api-overrides.mdx @@ -1,18 +1,11 @@ --- title: API Overrides -hide_title: true -sidebar_position: 3 -toc_max_heading_level: 4 -description: >- - Override APIs to control frontend-backend interactions and customize - input/output specifications. -page_type: sdk-reference -category: backend-sdk +description: Override APIs to control frontend-backend interactions and customize input/output specifications. +sidebar: + icon: server + order: 30 --- - -# APIs override - ## Overview Overriding APIs allows you to take full control of what happens when the frontend SDK calls the backend authentication endpoints. @@ -23,86 +16,76 @@ You can send analytics events, synchronize additional information in the databas ## General example -Like with the [functions override](/docs/references/backend-sdks/function-overrides) feature, the original implementation reference must be called to avoid any errors in the authentication flow. - - - +Like with the [functions override](/references/backend-sdks/function-overrides) feature, the original implementation reference must be called to avoid any errors in the authentication flow. + + ```ts import SuperTokens from "supertokens-node"; import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - - // only overriding the function that signs out a user - signOutPOST: async function (input) { - - if (originalImplementation.signOutPOST === undefined) { - throw Error("Should never come here") - } - // TODO: some custom logic - - // or call the default behaviour as show below - return await originalImplementation.signOutPOST(input); - }, - // ... - // TODO: override more apis - } - } - } - // highlight-end - }), - EmailPassword.init({ - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - emailExistsGET: async function (input) { - - // highlight-start - // send a custom response like this: - input.options.res.setStatusCode(200); // or any other status code - input.options.res.sendJSONResponse({ - message: "my custom response", - //... - }) - // highlight-end - - // this return doesn't matter. But we must do it - // cause the function signature expects a response. - return { - status: "OK", - exists: false - }; - } - } - } - } - }) - ] -}); - -``` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + Session.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + + // only overriding the function that signs out a user + signOutPOST: async function (input) { + if (originalImplementation.signOutPOST === undefined) { + throw Error("Should never come here"); + } + // TODO: some custom logic - - + // or call the default behaviour as show below + return await originalImplementation.signOutPOST(input); + }, + // ... + // TODO: override more apis + }; + }, + }, + }), + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + emailExistsGET: async function (input) { + // send a custom response like this: + input.options.res.setStatusCode(200); // or any other status code + input.options.res.sendJSONResponse({ + message: "my custom response", + //... + }); + // this return doesn't matter. But we must do it + // cause the function signature expects a response. + return { + status: "OK", + exists: false, + }; + }, + }; + }, + }, + }), + ], +}); +``` + + ```go import ( "encoding/json" @@ -118,7 +101,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start Override: &sessmodels.OverrideStruct{ APIs: func(originalImplementation sessmodels.APIInterface) sessmodels.APIInterface { // First we make a copy of the original implementation @@ -135,7 +117,6 @@ func main() { return originalImplementation }, }, - //highlight-end }), emailpassword.Init(&epmodels.TypeInput{ Override: &epmodels.OverrideStruct{ @@ -143,7 +124,6 @@ func main() { (*originalImplementation.EmailExistsGET) = func(email, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.EmailExistsGETResponse, error) { - // highlight-start // create a custom response. options.Res.Header().Set("Content-Type", "application/json; charset=utf-8") options.Res.WriteHeader(200) @@ -154,7 +134,6 @@ func main() { bytes, _ := json.Marshal(responseJson) options.Res.Write(bytes) - // highlight-end // this return doesn't matter. But we must do it // cause the function signature expects a response. @@ -173,12 +152,10 @@ func main() { }) } ``` - - - - + + ```python -from supertokens_python import init, InputAppInfo +from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe.emailpassword.interfaces import APIOptions as EmailPasswordAPIOptions, EmailExistsGetOkResult, APIInterface as EmailPasswordAPIInterface from supertokens_python.recipe.session.interfaces import APIOptions as SessionAPIOptions, APIInterface as SessionAPIInterface from supertokens_python.recipe import emailpassword @@ -189,12 +166,10 @@ def override_emailpassword_apis(original_implementation: EmailPasswordAPIInterfa async def email_exists_get(email: str, tenant_id: str, api_options: EmailPasswordAPIOptions, user_context: Dict[str, Any]): - # highlight-start # send custom response like this - api_options.response.set_status_code(200) # type: ignore + api_options.response.set_status_code(200) json_dict = {'message': 'Custom response'} - api_options.response.set_json_content(json_dict) # type: ignore - # highlight-end + api_options.response.set_json_content(json_dict) # this return doesn't matter. But we must do it # cause the function signature expects a response. @@ -203,7 +178,6 @@ def override_emailpassword_apis(original_implementation: EmailPasswordAPIInterfa original_implementation.email_exists_get = email_exists_get return original_implementation -# highlight-start def override_session_apis(original_implementation: SessionAPIInterface): original_signout_post = original_implementation.signout_post @@ -221,24 +195,20 @@ def override_session_apis(original_implementation: SessionAPIInterface): return original_implementation -# highlight-end - init( + supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="fastapi", recipe_list=[ session.init( - # highlight-start override=session.InputOverrideConfig(apis=override_session_apis) - # highlight-end ), emailpassword.init(override=emailpassword.InputOverrideConfig(apis=override_emailpassword_apis)) ], ) ``` - - - + + --- @@ -251,44 +221,39 @@ For custom UI, you can read this response and display the message in an error UI The next example shows how to prevent the user from signing up unless their email is pre-approved by the application's admin. - - - + + ```ts import EmailPassword from "supertokens-node/recipe/emailpassword"; EmailPassword.init({ - override: { - apis: (oI) => { + override: { + apis: (oI) => { + return { + ...oI, + signUpPOST: async function (input) { + let email = input.formFields.find((i) => i.id === "email")!.value as string; + + if (emailNotAllowed(email)) { return { - ...oI, - signUpPOST: async function (input) { - let email = input.formFields.find(i => i.id === "email")!.value as string; - - if (emailNotAllowed(email)) { - // highlight-start - return { - status: "GENERAL_ERROR", - message: "You are not allowed to sign up. Please contact the app's admin to get permission" - } - // highlight-end - } - return oI.signUpPOST!(input); - } - } - } - } -}) + status: "GENERAL_ERROR", + message: "You are not allowed to sign up. Please contact the app's admin to get permission", + }; + } + return oI.signUpPOST!(input); + }, + }; + }, + }, +}); function emailNotAllowed(email: string) { - // TODO: your impl to check if email is allowed or not - return true; + // TODO: your impl to check if email is allowed or not + return true; } ``` - - - - + + ```go import ( "errors" @@ -316,13 +281,11 @@ func main() { } } if (emailNotAllowed(email)) { - // highlight-start return epmodels.SignUpPOSTResponse{ GeneralError: &supertokens.GeneralErrorResponse{ Message: "You are not allowed to sign up. Please contact the app's admin to get permission", }, }, nil - // highlight-end } return originalSignUp(formFields, tenantId, options, userContext) } @@ -338,10 +301,8 @@ func emailNotAllowed(email string) bool { return true } ``` - - - - + + ```python from supertokens_python.recipe import emailpassword from supertokens_python.recipe.emailpassword.interfaces import ( @@ -381,11 +342,9 @@ def override_apis(original_implementation: APIInterface): email = form_fields[i].value if is_not_allowed(email): - # highlight-start return GeneralErrorResponse( message="You are not allowed to sign up. Please contact the app's admin to get permission" ) - # highlight-end return await original_sign_up( form_fields, @@ -408,118 +367,107 @@ def is_not_allowed(email: str): emailpassword.init(override=emailpassword.InputOverrideConfig(apis=override_apis)) ``` - - - + + --- ## Disable APIs - - - + + To disable an API entirely, all you need to do is override the API implementation with `undefined`. For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: + + +To disable an API entirely, all you need to do is override the API implementation with `nil`. -= v8.0.0', value: 'greater' }, - { label: '< v8.0.0', value: 'lesser' }, - ]}> - +For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: + + +To disable an API entirely, all you need to do is override the API disable `bool` value to `True`. + +For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: + + + + + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInPOST: undefined, // disable sign in with email & password - signUpPOST: undefined, // disable sign up with email & password - } - } - } - // highlight-end - }), - ThirdParty.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInUpPOST: undefined // disable sign in & up with third party - } - } - } - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInPOST: undefined, // disable sign in with email & password + signUpPOST: undefined, // disable sign up with email & password + }; + }, + }, + }), + ThirdParty.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInUpPOST: undefined, // disable sign in & up with third party + }; + }, + }, + }), + ], }); ``` - - - - + + ```tsx import SuperTokens from "supertokens-node"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - // highlight-start - override: { - apis: (originalImplementation) => { - return { - ...originalImplementation, - signInUpPOST: undefined - } - } - } - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + signInUpPOST: undefined, + }; + }, + }, + }), + ], }); ``` - - -
- - - - -To disable an API entirely, all you need to do is override the API implementation with `nil`. - -For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: - + + + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -533,7 +481,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ thirdparty.Init(&tpmodels.TypeInput{ - //highlight-start Override: &tpmodels.OverrideStruct{ APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface { @@ -544,10 +491,8 @@ func main() { }, }, - //highlight-end }), emailpassword.Init(&epmodels.TypeInput{ - //highlight-start Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { @@ -560,21 +505,15 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - -To disable an API entirely, all you need to do is override the API disable `bool` value to `True`. - -For example, if you want to disable the sign up / sign in API from this recipe, all you do is this: - + + ```python -from supertokens_python import init, InputAppInfo +from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword from supertokens_python.recipe.thirdparty.interfaces import ( APIInterface as ThirdPartyAPIInterface, @@ -584,16 +523,13 @@ from supertokens_python.recipe.emailpassword.interfaces import ( ) -# highlight-start def thirdparty_apis_override(original_impl: ThirdPartyAPIInterface): # disable sign in & up with third party original_impl.disable_sign_in_up_post = True return original_impl -# highlight-end -# highlight-start def emailpassword_apis_override(original_impl: EmailPasswordAPIInterface): # disable sign up with email & password original_impl.disable_sign_up_post = True @@ -601,33 +537,28 @@ def emailpassword_apis_override(original_impl: EmailPasswordAPIInterface): # disable sign in with email & password original_impl.disable_sign_in_post = True return original_impl -# highlight-end init( + supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="fastapi", recipe_list=[ thirdparty.init( - # highlight-start override=thirdparty.InputOverrideConfig(apis=thirdparty_apis_override), - # highlight-end ), emailpassword.init( - # highlight-start override=emailpassword.InputOverrideConfig( apis=emailpassword_apis_override ), - # highlight-end ), ], ) ``` + + - - - -:::info Important -You then need to define routes that handle this API call. You can see the [Frontend driver interface API spec here](/docs/references/fdi/introduction) +:::info[Important] +You then need to define routes that handle this API call. You can see the [Frontend driver interface API spec here](/references/fdi/introduction) ::: @@ -635,58 +566,59 @@ You then need to define routes that handle this API call. You can see the [Front ## Read custom request information - - - + + The `getRequestFromUserContext` function provided by the SDK is used to get the request object from the user context. + + +The `GetRequestFromUserContext` function provided by the SDK is used to get the request object from the user context. + + +The `get_request_from_user_context` function provided by the SDK is used to get the request object from the user context. + + + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; Session.init({ - override: { - apis: (oI) => { - return { - ...oI, - signOutPOST: async (input) => { - if (oI.signOutPOST === undefined) { - throw Error("Signout API is disabled"); - } - - // highlight-start - let customHeaderValue = ""; - const request = SuperTokens.getRequestFromUserContext(input.userContext); - - if (request !== undefined) { - //@ts-ignore - customHeaderValue = request.getHeaderValue("customHeader"); - } else { - /** - * This is possible if the function is triggered from the user management dashboard - * - * In this case set a reasonable default value to use - */ - customHeaderValue = "default"; - } - - // highlight-end - - // Perform custom logic based on the value of customHeaderValue - - return oI.signOutPOST(input); - }, - }; - } + override: { + apis: (oI) => { + return { + ...oI, + signOutPOST: async (input) => { + if (oI.signOutPOST === undefined) { + throw Error("Signout API is disabled"); + } + + let customHeaderValue = ""; + const request = SuperTokens.getRequestFromUserContext(input.userContext); + + if (request !== undefined) { + customHeaderValue = request.getHeaderValue("customHeader") ?? ""; + } else { + /** + * This is possible if the function is triggered from the user management dashboard + * + * In this case set a reasonable default value to use + */ + customHeaderValue = "default"; + } + + // Perform custom logic based on the value of customHeaderValue + + return oI.signOutPOST(input); + }, + }; }, -}) + }, +}); ``` - - - - -The `GetRequestFromUserContext` function provided by the SDK is used to get the request object from the user context. - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -701,7 +633,6 @@ func main() { originalSignOutPost := *originalImplementation.SignOutPOST *originalImplementation.SignOutPOST = func(sessionContainer sessmodels.SessionContainer, options sessmodels.APIOptions, userContext supertokens.UserContext) (sessmodels.SignOutPOSTResponse, error) { - // highlight-start customHeadervalue := "" request := supertokens.GetRequestFromUserContext(userContext) @@ -715,7 +646,6 @@ func main() { */ customHeadervalue = "default"; } - // highlight-end print(customHeadervalue) @@ -730,12 +660,8 @@ func main() { }) } ``` - - - - -The `get_request_from_user_context` function provided by the SDK is used to get the request object from the user context. - + + ```python from supertokens_python import get_request_from_user_context from supertokens_python.recipe import session @@ -747,7 +673,6 @@ def override_session_apis(original_implementation: APIInterface): original_signout_post = original_implementation.signout_post async def signout_post(session: session.SessionContainer, api_options: APIOptions, user_context: Dict[str, Any]): - # highlight-start request=get_request_from_user_context(user_context) customHeaderValue="" @@ -760,7 +685,6 @@ def override_session_apis(original_implementation: APIInterface): # In this case set a reasonable default value to use # customHeaderValue="default" - # highlight-end print(customHeaderValue) # Perform custom logic based on the value of customHeadervalue @@ -776,6 +700,5 @@ session.init( ), ) ``` - - - + + diff --git a/docs/references/backend-sdks/backend-sdk-core-interceptor.mdx b/docs/references/backend-sdks/backend-sdk-core-interceptor.mdx index 43e2afee52..b30e455fde 100644 --- a/docs/references/backend-sdks/backend-sdk-core-interceptor.mdx +++ b/docs/references/backend-sdks/backend-sdk-core-interceptor.mdx @@ -1,17 +1,11 @@ --- title: Network interceptor -hide_title: true -sidebar_position: 7 -description: >- - Intercept and modify outgoing backend SDK requests to the core in NodeJS, - Python, and GoLang. -page_type: sdk-reference -category: backend-sdk +description: Intercept and modify outgoing backend SDK requests to the core in NodeJS, Python, and GoLang. +sidebar: + icon: network + order: 60 --- - -# Backend SDK core interceptor - ## Overview This hook intercepts all outgoing requests from the backend SDK to the core. @@ -20,7 +14,7 @@ Users can modify the HTTP method, query params, headers, and body of the request ## Prerequisites -:::info Important +:::info[Important] This feature is only available for SDKs versions: - NodeJS >= `v16.5.0` - Python >= `v0.16.8` @@ -29,39 +23,34 @@ This feature is only available for SDKs versions: ## Example - - - + + ```tsx import { HttpRequest } from "supertokens-node/types"; import SuperTokens from "supertokens-node"; SuperTokens.init({ - supertokens: { - connectionURI: "...", - apiKey: "...", - // highlight-start - networkInterceptor: (request: HttpRequest, userContext: any) => { - console.log("http request to core: ", request) - // this can also be used to return a modified request object. - return request; - }, - // highlight-end - }, - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", + supertokens: { + connectionURI: "...", + apiKey: "...", + networkInterceptor: (request: HttpRequest, userContext: any) => { + console.log("http request to core: ", request); + // this can also be used to return a modified request object. + return request; }, - recipeList: [ - // ... - ], + }, + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + // ... + ], }); ``` - - - - + + ```go import ( "log" @@ -75,12 +64,10 @@ supertokens.Init(supertokens.TypeInput{ Supertokens: &supertokens.ConnectionInfo{ ConnectionURI: "...", APIKey: "...", - // highlight-start NetworkInterceptor: func(request *http.Request, context supertokens.UserContext) (*http.Request, error) { log.Print("http request to core: %+v", request) return request, nil }, - // highlight-end }, AppInfo: supertokens.AppInfo{ AppName: "...", @@ -91,15 +78,12 @@ supertokens.Init(supertokens.TypeInput{ }) } ``` - - - - + + ```python from typing import Dict, Any, Optional from supertokens_python import init, InputAppInfo, SupertokensConfig -# highlight-start def intercept( url: str, method: str, @@ -110,7 +94,6 @@ def intercept( ): print("http request to core: ", url, method, headers, params, body) return url, method, headers, params, body -# highlight-end init( app_info=InputAppInfo( @@ -121,7 +104,6 @@ init( supertokens_config=SupertokensConfig( connection_uri="...", api_key="...", - # highlight-next-line network_interceptor=intercept, ), framework="django", # works with other frameworks as well @@ -130,6 +112,5 @@ init( ], ) ``` - - - + + diff --git a/docs/references/backend-sdks/function-overrides.mdx b/docs/references/backend-sdks/function-overrides.mdx index 9131278fc8..1ee89e880c 100644 --- a/docs/references/backend-sdks/function-overrides.mdx +++ b/docs/references/backend-sdks/function-overrides.mdx @@ -1,17 +1,11 @@ --- title: Function Overrides -hide_title: true -sidebar_position: 2 -description: >- - Override backend authentication functions to implement cover custome use - cases. -page_type: sdk-reference -category: backend-sdk +description: Override backend authentication functions to cover custom use cases. +sidebar: + icon: code + order: 20 --- - -# Function overrides - ## Overview **Function overrides** let you customize the behavior of the functions used internally, by the SDKs. @@ -23,94 +17,97 @@ Similarly, if you already have a sign-in/sign-up flow and want to integrate with You can even implement a custom `userId` format by mapping `userIds` to those generated by SuperTokens. - ## Example The code snippet shows the general flow of overriding a function. Custom logic can be injected while also calling the original implementation of the function. - - - + + +:::info +See all the [functions that can be overridden here](https://supertokens.com/docs/references/backend-sdks/function-overrides) +::: + + :::info -See all the [functions that can be overridden here](https://supertokens.com/docs/nodejs/modules/recipe_thirdparty.html#RecipeInterface) +See all the [functions that can be overridden here](https://pkg.go.dev/github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels#RecipeInterface) +::: + + +:::info +See all the [functions that can be overridden here](https://supertokens.com/docs/references/backend-sdks/function-overrides) ::: + + + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; import ThirdParty from "supertokens-node/recipe/thirdparty"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - Session.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // here we are only overriding the function that's responsible - // for creating a new session - createNewSession: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return await originalImplementation.createNewSession(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }), - ThirdParty.init({ - signInAndUpFeature: { - providers: [/* ... */] - }, - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // here we are only overriding the function that's responsible - // for signing in or signing up a user. - signInUp: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return await originalImplementation.signInUp(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }) - ] -}); -``` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + // here we are only overriding the function that's responsible + // for creating a new session + createNewSession: async function (input) { + // TODO: some custom logic - - + // or call the default behaviour as show below + return await originalImplementation.createNewSession(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + /* ... */ + ], + }, + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, -:::info -See all the [functions that can be overridden here](https://pkg.go.dev/github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels#RecipeInterface) -::: + // here we are only overriding the function that's responsible + // for signing in or signing up a user. + signInUp: async function (input) { + // TODO: some custom logic + // or call the default behaviour as show below + return await originalImplementation.signInUp(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + ], +}); +``` + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -124,7 +121,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ session.Init(&sessmodels.TypeInput{ - //highlight-start Override: &sessmodels.OverrideStruct{ Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface { // First we make a copy of the original implementation @@ -141,10 +137,8 @@ func main() { return originalImplementation }, }, - //highlight-end }), thirdparty.Init(&tpmodels.TypeInput{ - //highlight-start Override: &tpmodels.OverrideStruct{ Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { @@ -164,24 +158,17 @@ func main() { return originalImplementation }, }, - //highlight-end }), }, }) } ``` - - - - -:::info -See all the [functions that can be overridden here](https://supertokens.com/docs/python/recipe/thirdparty/interfaces.html#supertokens_python.recipe.thirdparty.interfaces.RecipeInterface) -::: - + + ```python from typing import Any, Dict, Optional, Union -from supertokens_python import InputAppInfo, init +from supertokens_python import InputAppInfo, SupertokensConfig, init from supertokens_python.recipe import session, thirdparty from supertokens_python.recipe.session.interfaces import ( RecipeInterface as SessionRecipeInterface, @@ -194,7 +181,6 @@ from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider from supertokens_python.types import RecipeUserId -# highlight-start def override_thirdparty_functions(original_implementation: ThirdPartyRecipeInterface): original_sign_in_up = original_implementation.sign_in_up @@ -229,7 +215,6 @@ def override_thirdparty_functions(original_implementation: ThirdPartyRecipeInter original_implementation.sign_in_up = sign_in_up return original_implementation -# highlight-start def override_session_functions(original_implementation: SessionRecipeInterface): original_create_new_session = original_implementation.create_new_session @@ -259,26 +244,18 @@ def override_session_functions(original_implementation: SessionRecipeInterface): return original_implementation -# highlight-end - - -# highlight-end - init( + supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="fastapi", recipe_list=[ session.init( - # highlight-start override=session.InputOverrideConfig(functions=override_session_functions) - # highlight-end ), thirdparty.init( - # highlight-start override=thirdparty.InputOverrideConfig( functions=override_thirdparty_functions ), - # highlight-end sign_in_and_up_feature=thirdparty.SignInAndUpFeature( providers=[ # ... @@ -288,9 +265,8 @@ init( ], ) ``` - - - + + --- @@ -300,38 +276,33 @@ If you want to throw a custom error from function overrides you have to handle i ### Raise the error - - - + + ```ts import Session from "supertokens-node/recipe/session"; Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - createNewSession: async function (input) { - //highlight-start - const existingSessions = await Session.getAllSessionHandlesForUser(input.userId); - if (existingSessions.length > 0) { - // this means that the user already has a session on some other device - throw new Error("Session already exists on another device"); - } - - // no other session exists, and so we can continue with logging in this user - return originalImplementation.createNewSession(input); - //highlight-end - } - } - } - } -}) + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + createNewSession: async function (input) { + const existingSessions = await Session.getAllSessionHandlesForUser(input.userId); + if (existingSessions.length > 0) { + // this means that the user already has a session on some other device + throw new Error("Session already exists on another device"); + } + + // no other session exists, and so we can continue with logging in this user + return originalImplementation.createNewSession(input); + }, + }; + }, + }, +}); ``` - - - - + + ```go import ( "errors" @@ -350,7 +321,6 @@ func main() { originalCreateNewSession := *originalImplementation.CreateNewSession (*originalImplementation.CreateNewSession) = func(userID string, accessTokenPayload, sessionDataInDatabase map[string]interface{}, disableAntiCsrf *bool, tenantId string, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) { - // highlight-start existingSessions, err := session.GetAllSessionHandlesForUser(userID, &tenantId, userContext) if err != nil { return nil, err @@ -363,7 +333,6 @@ func main() { // no other session exists, and so we can continue with logging in this user return originalCreateNewSession(userID, accessTokenPayload, sessionDataInDatabase, disableAntiCsrf, tenantId, userContext) - // highlight-end } return originalImplementation @@ -373,10 +342,8 @@ func main() { }) } ``` - - - - + + ```python from typing import Any, Dict, Optional @@ -400,7 +367,6 @@ def override_session_functions(original_implementation: RecipeInterface): tenant_id: str, user_context: Dict[str, Any], ): - # highlight-start existing_sessions = await get_all_session_handles_for_user(user_id) if len(existing_sessions) > 0: @@ -417,7 +383,6 @@ def override_session_functions(original_implementation: RecipeInterface): tenant_id, user_context, ) - # highlight-end original_implementation.create_new_session = create_new_session @@ -426,17 +391,25 @@ def override_session_functions(original_implementation: RecipeInterface): session.init(override=session.InputOverrideConfig(functions=override_session_functions)) ``` - - - + + ### Handle the error manually - - - - - + + + + + + + + + + + + + + ```ts import express from "express"; @@ -446,89 +419,85 @@ let app = express(); // in your app's error handler, we catch the custom error app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - if (err.message === "Session already exists on another device") { - // TODO: send a custom response using res - return; - } - res.send(500).send(err.message) -}) + if (err.message === "Session already exists on another device") { + // TODO: send a custom response using res + return; + } + res.status(500).send(err.message); +}); ``` - - - - + + ```ts -import Hapi from "hapi"; +import Hapi from "@hapi/hapi"; let server = new Hapi.Server({ port: 8000 }); // first we create a plugin to handle all errors from the app const plugin = { - name: "...", - version: "...", - register: async function (server: Hapi.Server) { - server.ext("onPreResponse", async (request, h) => { - if ("isBoom" in request.response) { - let err = request.response.data; - if (err.message === "Session already exists on another device") { - // TODO: send a custom response here with takeover - } - } - return h.continue; - }); - }, + name: "...", + version: "...", + register: async function (server: Hapi.Server) { + server.ext("onPreResponse", async (request, h) => { + if ("isBoom" in request.response) { + let err = request.response.data; + if (err.message === "Session already exists on another device") { + // TODO: send a custom response here with takeover + } + } + return h.continue; + }); + }, }; // then we register this plugin (async () => { - await server.register(plugin); + await server.register(plugin); - await server.start(); + await server.start(); })(); ``` - - - + + ```tsx import Fastify from "fastify"; let fastify = Fastify(); fastify.setErrorHandler(async (err: any, req, res) => { - if (err.message === "Session already exists on another device") { - // TODO: send a custom response here with takeover - } - // TODO: send a 500 error with the err.message + if (err.message === "Session already exists on another device") { + // TODO: send a custom response here with takeover + } + // TODO: send a 500 error with the err.message }); ``` - - - - -```ts + + +```ts check=false reason="AWS Lambda handler imports application-local backend configuration" import middy from "@middy/core"; import cors from "@middy/http-cors"; import SuperTokens from "supertokens-node"; // this is in the auth.js file import { middleware } from "supertokens-node/framework/awsLambda"; -// @ts-ignore import { getBackendConfig } from "./config"; -module.exports.handler = middy(middleware()).use(cors({ - origin: getBackendConfig().appInfo.websiteDomain, - credentials: true, - headers: ["Content-Type", ...SuperTokens.getAllCORSHeaders()].join(", "), - methods: "OPTIONS,POST,GET,PUT,DELETE" -})).onError(request => { +module.exports.handler = middy(middleware()) + .use( + cors({ + origin: getBackendConfig().appInfo.websiteDomain, + credentials: true, + headers: ["Content-Type", ...SuperTokens.getAllCORSHeaders()].join(", "), + methods: "OPTIONS,POST,GET,PUT,DELETE", + }), + ) + .onError((request) => { if (request.error !== null && request.error.message === "Session already exists on another device") { - // TODO: send a custom response here with takeover + // TODO: send a custom response here with takeover } throw request.error; -}); + }); ``` - - - - + + ```ts import Koa from "koa"; import { middleware } from "supertokens-node/framework/koa"; @@ -544,14 +513,12 @@ app.use(async (ctx, next) => { } throw err; } -}) +}); app.use(middleware()); ``` - - - - + + ```ts import { Next } from "@loopback/core"; import { RestApplication, Middleware, MiddlewareContext } from "@loopback/rest"; @@ -573,14 +540,8 @@ export const customErrorMiddleware: Middleware = async (ctx: MiddlewareContext, app.middleware(middleware); app.middleware(customErrorMiddleware); ``` - - - - - - - - + + ```ts @@ -589,104 +550,55 @@ import { middleware } from "supertokens-node/framework/express"; // in the /auth/[[...path]].tsx file export default async function superTokens(req: any, res: any) { - //... - try { - await superTokensNextWrapper( - async (next) => { - // Refer to the Next.js integration guide to know why this is needed - res.setHeader( - "Cache-Control", - "no-cache, no-store, max-age=0, must-revalidate" - ); - await middleware()(req, res, next) - }, - req, - res - ) - } catch (err: any) { - if (err.message === "Session already exists on another device") { - // TODO: send custom reply - } - throw err; - } - - //... -} -``` - - - - -```ts -import { getAppDirRequestHandler } from "supertokens-node/nextjs"; -import { NextRequest, NextResponse } from "next/server"; -import SuperTokens from "supertokens-node"; -// @ts-ignore -import { backendConfig } from "@/app/config/backend"; - -SuperTokens.init(backendConfig()); - -// in the app/api/auth/[...path]/route.ts file -const handleCall = getAppDirRequestHandler(); - -const withCustomErrorHandling = async (request: NextRequest) => { - try { - return await handleCall(request); - } catch (err: any) { - if (err.message === "Session already exists on another device") { - // TODO: send custom reply - } - throw err; - } -}; - -export async function GET(request: NextRequest) { - const res = await withCustomErrorHandling(request); - if (!res.headers.has("Cache-Control")) { + //... + try { + await superTokensNextWrapper( + async (next) => { // Refer to the Next.js integration guide to know why this is needed - res.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + res.setHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + await middleware()(req, res, next); + }, + req, + res, + ); + } catch (err: any) { + if (err.message === "Session already exists on another device") { + // TODO: send custom reply } - return res; -} + throw err; + } -export const POST = withCustomErrorHandling; -export const DELETE = withCustomErrorHandling; -export const PUT = withCustomErrorHandling; -export const PATCH = withCustomErrorHandling; -export const HEAD = withCustomErrorHandling; + //... +} ``` - - - - + + ```tsx -import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'; +import { ExceptionFilter, Catch, ArgumentsHost } from "@nestjs/common"; -import { errorHandler } from 'supertokens-node/framework/express'; -import { Error as STError } from 'supertokens-node'; +import { errorHandler } from "supertokens-node/framework/express"; +import { Error as STError } from "supertokens-node"; // we want to add our own error handler which will catch the special exception @Catch(STError) export class AppErrorHandler implements ExceptionFilter { - catch(exception: Error, host: ArgumentsHost) { const ctx = host.switchToHttp(); if (exception.message === "Session already exists on another device") { - // TODO: send custom error using ctx.getResponse() + // TODO: send custom error using ctx.getResponse() } else { - throw exception; + throw exception; } } } ``` - - - - - + + + + ```go import ( "net/http" @@ -705,59 +617,54 @@ func main() { }) } ``` - - - - - - + + + + ```python -from fastapi import FastAPI +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse app = FastAPI() -@app.exception_handler(Exception) # type: ignore -async def exception_handler(_, exc: Exception): +@app.exception_handler(Exception) +async def exception_handler(_: Request, exc: Exception) -> JSONResponse: if str(exc) == "Session already exists on another device": - pass # TODO: send custom response + return JSONResponse({"message": str(exc)}, status_code=409) - # TODO: Send generic 500 response + return JSONResponse({"message": "Internal server error"}, status_code=500) ``` - - - - + + ```python from flask import Flask app = Flask(__name__) -@app.errorhandler(Exception) # type: ignore -def all_exception_handler(exc: Exception): - if str(exc) == "Session already exists on another device": - pass # TODO: send custom response +@app.errorhandler(Exception) +def all_exception_handler(error: Exception): + if str(error) == "Session already exists on another device": + return {"message": str(error)}, 409 - # TODO: Send generic 500 response + return {"message": "Internal server error"}, 500 ``` - - - - + + ```python # Add this middlware in settings.py -from typing import cast +from typing import Callable from django.http import HttpRequest, HttpResponse class ErrorHandlerMiddleware: - def __init__(self, get_response): # type: ignore + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): self.get_response = get_response def __call__(self, request: HttpRequest): - response: HttpResponse = cast(HttpResponse, self.get_response(request)) + response = self.get_response(request) return response def process_exception(self, request: HttpRequest, exception: Exception) -> HttpResponse: @@ -766,65 +673,116 @@ class ErrorHandlerMiddleware: return HttpResponse("Error processing the request.", status=500) ``` + + + + + + + + + + + +```ts check=false reason="Next.js route imports application-local backend configuration" +import { getAppDirRequestHandler } from "supertokens-node/nextjs"; +import { NextRequest, NextResponse } from "next/server"; +import SuperTokens from "supertokens-node"; +import { backendConfig } from "@/app/config/backend"; + +SuperTokens.init(backendConfig()); + +// in the app/api/auth/[...path]/route.ts file +const handleCall = getAppDirRequestHandler(); + +const withCustomErrorHandling = async (request: NextRequest) => { + try { + return await handleCall(request); + } catch (err: any) { + if (err.message === "Session already exists on another device") { + // TODO: send custom reply + } + throw err; + } +}; + +export async function GET(request: NextRequest) { + const res = await withCustomErrorHandling(request); + if (!res.headers.has("Cache-Control")) { + // Refer to the Next.js integration guide to know why this is needed + res.headers.set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"); + } + return res; +} + +export const POST = withCustomErrorHandling; +export const DELETE = withCustomErrorHandling; +export const PUT = withCustomErrorHandling; +export const PATCH = withCustomErrorHandling; +export const HEAD = withCustomErrorHandling; +``` + - - - - + + + + + --- ## Read custom request information - - - + + We use the `getRequestFromUserContext` function provided by the SDK to get the request object from the user context. + + +We use the `GetRequestFromUserContext` function provided by the SDK to get the request object from the user context. + + +We use the `get_request_from_user_context` function provided by the SDK to get the request object from the user context. + + + + ```tsx import SuperTokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; Session.init({ - override: { - functions: (oI) => { - return { - ...oI, - revokeSession: async (input) => { - // highlight-start - let customHeaderValue = ""; - const request = SuperTokens.getRequestFromUserContext(input.userContext); - - if (request !== undefined) { - //@ts-ignore - customHeaderValue = request.getHeaderValue("customHeader"); - } else { - /** - * This is possible if the function is triggered from the user management dashboard - * - * In this case set a reasonable default value to use - */ - customHeaderValue = "default"; - } - - // highlight-end - - // Perform custom logic based on the value of customHeaderValue - - return oI.revokeSession(input); - }, - }; + override: { + functions: (oI) => { + return { + ...oI, + revokeSession: async (input) => { + let customHeaderValue = ""; + const request = SuperTokens.getRequestFromUserContext(input.userContext); + + if (request !== undefined) { + customHeaderValue = request.getHeaderValue("customHeader") ?? ""; + } else { + /** + * This is possible if the function is triggered from the user management dashboard + * + * In this case set a reasonable default value to use + */ + customHeaderValue = "default"; + } + + // Perform custom logic based on the value of customHeaderValue + + return oI.revokeSession(input); }, + }; }, -}) + }, +}); ``` - - - - -We use the `GetRequestFromUserContext` function provided by the SDK to get the request object from the user context. - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -839,7 +797,6 @@ func main() { originalRevokeSession := *originalImplementation.RevokeSession *originalImplementation.RevokeSession = func(sessionHandle string, userContext supertokens.UserContext) (bool, error) { - // highlight-start customHeadervalue := "" request := supertokens.GetRequestFromUserContext(userContext) @@ -853,7 +810,6 @@ func main() { */ customHeadervalue = "default"; } - // highlight-end print(customHeadervalue) @@ -868,12 +824,8 @@ func main() { }) } ``` - - - - -We use the `get_request_from_user_context` function provided by the SDK to get the request object from the user context. - + + ```python from typing import Any, Dict @@ -886,7 +838,6 @@ def override_session_functions(original_implementation: RecipeInterface): original_revoke_session = original_implementation.revoke_session async def revoke_session(session_handle: str, user_context: Dict[str, Any]): - # highlight-start request=get_request_from_user_context(user_context) customHeaderValue="" @@ -899,7 +850,6 @@ def override_session_functions(original_implementation: RecipeInterface): # In this case set a reasonable default value to use # customHeaderValue="default" - # highlight-end print(customHeaderValue) # Perform custom logic based on the value of customHeadervalue @@ -916,6 +866,5 @@ session.init( ), ) ``` - - - + + diff --git a/docs/references/backend-sdks/meta.ts b/docs/references/backend-sdks/meta.ts new file mode 100644 index 0000000000..f17595a12d --- /dev/null +++ b/docs/references/backend-sdks/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Backend SDKs", + icon: "server", + order: 50, +}); diff --git a/docs/references/backend-sdks/other-frameworks.mdx b/docs/references/backend-sdks/other-frameworks.mdx index a3577d716e..ad9a3bc76a 100644 --- a/docs/references/backend-sdks/other-frameworks.mdx +++ b/docs/references/backend-sdks/other-frameworks.mdx @@ -1,13 +1,9 @@ --- -id: other-frameworks title: Supporting other backend frameworks -hide_title: false -sidebar_position: 8 -description: >- - Set up an authentication server with SuperTokens for unsupported backend - frameworks. -page_type: guide -category: backend-sdk +description: Set up an authentication server with SuperTokens for unsupported backend frameworks. +sidebar: + icon: blocks + order: 80 --- If your backend framework is not supported by SuperTokens, you can follow this guide on setting up an authentication server to protect your frontend and backend. @@ -19,15 +15,19 @@ To illustrate how this setup works, consider an example: - SuperTokens does not have a PHP SDK, and a separate service in NodeJS starts to act as an authentication server. The SuperTokens backend SDK configures this server to handle authentication requests and the issuing of access and refresh tokens. - The architecture of setup should have the following design: -![SuperTokens Setup With Authentication server](/img/other-framework-setup.png) +SuperTokens setup with an authentication server - In the architecture above the the React Frontend communicates with the PHP and node server through a reverse proxy. - Authentication requests like sign-up or sign-in route to the NodeJS server. These APIs are automatically created and handled by the `supertokens-node` SDK. On the other hand, your application-specific APIs are on the PHP server. - Once a user signs up or signs in, the system creates a session between your API server's domain and the frontend. - Application requests to the PHP server have session tokens attached to them. -- The session token is a JWT, and the PHP server can [verify it](/docs/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). +- The session token is a JWT, and the PHP server can [verify it](/additional-verification/session-verification/protect-api-routes#using-a-jwt-verification-library). - When verifying a request, if the session token is missing or has expired, the PHP server should return a `401` response. This prompts the SuperTokens Frontend SDK to trigger the automatic refresh flow to generate new session tokens and retry the request. -:::info Important -The method mentioned above assumes that the authentication server is running on the same domain as the PHP server. If instead, your authentication server runs on a separate subdomain, you need to [enable cookie sharing](/docs/post-authentication/session-management/advanced-workflows/multiple-api-endpoints) for cookies to be automatically attached to requests to the PHP server. +:::info[Important] +The method mentioned above assumes that the authentication server is running on the same domain as the PHP server. If instead, your authentication server runs on a separate subdomain, you need to [enable cookie sharing](/post-authentication/session-management/advanced-workflows/multiple-api-endpoints) for cookies to be automatically attached to requests to the PHP server. ::: diff --git a/docs/references/backend-sdks/reference.mdx b/docs/references/backend-sdks/reference.mdx index e5e695f974..e09f6c21d1 100644 --- a/docs/references/backend-sdks/reference.mdx +++ b/docs/references/backend-sdks/reference.mdx @@ -1,16 +1,12 @@ --- title: Reference -hide_title: true -sidebar_position: 1 -description: >- - Override frontend authentication functions using SuperTokens for custom - session and user sign-up logic. -page_type: overview -category: backend-sdk +description: Find SuperTokens backend SDK references and customize authentication functions, APIs, and configuration. +sidebar: + icon: book-open + order: 10 --- - -# Backend SDKs +import { PresentedOption } from "../../../components/option-presentation"; ## Overview @@ -19,82 +15,54 @@ Use this page to find references to each of them and about specific functionalit ## Customization - - - - Function overrides - - - - - API overrides - - - - - - Hooks - - - - - - - - -## SDK references - - - - - - NodeJS - - - - - - - GoLang - - - - - - - Python - - - - + + + + + + +## SDKs + + + +

+ +
+ +

+ +
+ +

+ +
+
## SDK configuration -The `appInfo` object is the paramter used to configure the SDKs during initialization. -```ts +The `appInfo` object is the parameter used to configure the SDKs during initialization. + +```ts check=false reason="configuration type excerpt references the SDK UserContext type" let appInfo: { - appName: string, - apiDomain: string, - websiteDomain?: string, - websiteBasePath?: string, - origin?: ((input: { request?: Request; userContext: UserContext }) => string) | string, - apiBasePath?: string, - apiGatewayPath?: string -} + appName: string; + apiDomain: string; + websiteDomain?: string; + websiteBasePath?: string; + origin?: ((input: { request?: Request; userContext: UserContext }) => string) | string; + apiBasePath?: string; + apiGatewayPath?: string; +}; ``` - - -## `appName` - + + This is the name of your application. Use it when sending password reset or email verification emails (in the default email design). An example of this is `appName: "GitHub"`. -
- -## `websiteDomain` - +
+
+ This is the domain part of your website. This is where the login UI appears. For example: @@ -107,10 +75,9 @@ By default, the login UI appears on `{websiteDomain}/auth/*`. You can change thi On the frontend, you need the domain for routing purposes, and on the backend, it generates correct email verification and password reset links. -
- -## `apiDomain` - +
+
+ This is the domain part of your API endpoint that the frontend talks to. For example: - For local development, you are likely using `localhost` with some port (ex `9000`). Then the value of this should be `"http://localhost:9000"`. - If your frontend queries `https://api.example.com/*`, then the value of this should be `"https://api.example.com"` @@ -118,32 +85,28 @@ This is the domain part of your API endpoint that the frontend talks to. For exa By default, the login widgets query `{apiDomain}/auth/*`. You can change this by using the `apiBasePath` configuration. -
- -## `websiteBasePath` - +
+
+ By default, the login UI appears on `{websiteDomain}/auth`. Other authentication-related user interfaces appear on `{websiteDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: - If you want the login UI to show on `{websiteDomain}/user/*`, then the value of this should be `"/user"`. - If you are using a dedicated sub domain for auth, like `https://auth.example.com`, then you probably want the login UI to show up on `https://auth.example.com`. In this case, set this value to `"/"`. -:::important -Remember to set the same value for this parameter on the backend and the frontend. +:::note[Remember to set the same value for this parameter on the backend and the frontend.] ::: -
- -## `origin` - +
+
+ Can be used instead of `websiteDomain` to handle multiple frontend domains within the same SuperTokens instance. The property accepts a function that receives the original request as an input and should return a valid domain. Make sure to whitelist all the domains during CORS configuration. -
- -## `apiBasePath` - +
+
+ By default, the frontend SDK queries `{apiDomain}/auth/*`. If you want to change the `/auth` to something else, then you must set this value. For example: @@ -151,23 +114,19 @@ If you want to change the `/auth` to something else, then you must set this valu - If you want to scope the APIs not via `/auth` but via some other string like `/supertokens`, then you can set the value of this to `"/supertokens"`. This means, the APIs appear on `{apiDomain}/supertokens/*`. - If you do not want to scope the APIs at all, then you can set the values of this to be `"/"`. This means the APIs are available on `{apiDomain}/*` -:::important -Remember to set the same value for this parameter on the backend and the frontend. +:::note[Remember to set the same value for this parameter on the backend and the frontend.] ::: -:::caution -Note that setting a custom `apiBasePath` updates the refresh API path, this can cause an issue where previously issued refresh tokens no longer get sent to the new API endpoint and the user logs out. +:::warning[Note that setting a custom `apiBasePath` updates the refresh API path, this can cause an issue where previously issued refresh tokens no longer get sent to the new API endpoint and the user logs out.] For example, the default `apiBasePath` value is `/auth`, if it changes to `/supertokens`, then your refresh endpoint updates from `/auth/session/refresh` to `/supertokens/session/refresh`. Previously issued refresh tokens do not get sent to the new API endpoint and the user logs out. ::: -
- -## `apiGatewayPath` - -:::note -Most relevant if you are using an API gateway or reverse proxy +
+
+ +:::note[Most relevant if you are using an API gateway or reverse proxy] ::: If you are using an API gateway (like the one provided by AWS) or a reverse proxy (like Nginx), it may add a path to your API endpoints to scope them for different development environments. @@ -183,7 +142,6 @@ This means that the frontend SDK queries `{apiDomain}/development/supertokens/*` The reason for this distinction between `apiGatewayPath` and `apiBasePath` is that when routing, the backend SDK does not see the `apiGatewayPath` path from the request because the gateway removes them. Taking the above example, whilst the frontend queries `{apiDomain}/development/supertokens/*`, the backend SDK sees `{apiDomain}/supertokens/*`. -
- +
+
- diff --git a/docs/references/backend-sdks/supertokens-nodejs/_category_.json b/docs/references/backend-sdks/supertokens-nodejs/_category_.json deleted file mode 100644 index 55490c19e5..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/_category_.json +++ /dev/null @@ -1 +0,0 @@ -{"label":"Node.js SDK Reference","position":7} \ No newline at end of file diff --git a/docs/references/backend-sdks/supertokens-nodejs/package.mdx b/docs/references/backend-sdks/supertokens-nodejs/package.mdx deleted file mode 100644 index f382a20738..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/package.mdx +++ /dev/null @@ -1,945 +0,0 @@ ---- -title: supertokens-node -page_title: SuperTokens Node -sidebar_position: 1 -description: References documentation for the supertokens-node package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# SuperTokens Node - - -## Classes - -### default - -Defined in: [index.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L33) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [index.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L36) | -| `init` | `static` | (`config`) => `void` | `SuperTokens.init` | [index.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L34) | -| `RecipeUserId` | `static` | *typeof* [`RecipeUserId`](#recipeuserid-1) | `RecipeUserId` | [index.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L37) | -| `User` | `static` | *typeof* [`User`](#user-1) | `User` | [index.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L38) | - -#### Methods - -##### convertToRecipeUserId() - -```ts -static convertToRecipeUserId(recipeUserId): RecipeUserId; -``` - -Defined in: [index.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L167) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | `string` | - -###### Returns - -[`RecipeUserId`](#recipeuserid-1) - -##### createUserIdMapping() - -```ts -static createUserIdMapping(input): Promise< - | { - status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; -} - | { - doesExternalUserIdExist: boolean; - doesSuperTokensUserIdExist: boolean; - status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"; -}>; -``` - -Defined in: [index.ts:88](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L88) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `externalUserId`: `string`; `externalUserIdInfo?`: `string`; `force?`: `boolean`; `superTokensUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.externalUserId` | `string` | -| `input.externalUserIdInfo?` | `string` | -| `input.force?` | `boolean` | -| `input.superTokensUserId` | `string` | -| `input.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() - -```ts -static deleteUser( - userId, - removeAllLinkedAccounts, - userContext?): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [index.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L156) - -###### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `userId` | `string` | `undefined` | -| `removeAllLinkedAccounts` | `boolean` | `true` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### deleteUserIdMapping() - -```ts -static deleteUserIdMapping(input): Promise<{ - didMappingExist: boolean; - status: "OK"; -}>; -``` - -Defined in: [index.ts:112](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L112) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `force?`: `boolean`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.force?` | `boolean` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -###### Returns - -`Promise`\<\{ - `didMappingExist`: `boolean`; - `status`: `"OK"`; -\}\> - -##### getAllCORSHeaders() - -```ts -static getAllCORSHeaders(): string[]; -``` - -Defined in: [index.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L40) - -###### Returns - -`string`[] - -##### getRequestFromUserContext() - -```ts -static getRequestFromUserContext(userContext): BaseRequest; -``` - -Defined in: [index.ts:171](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L171) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`BaseRequest` - -##### getUser() - -```ts -static getUser(userId, userContext?): Promise; -``` - -Defined in: [index.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L136) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -##### getUserCount() - -```ts -static getUserCount( - includeRecipeIds?, - tenantId?, -userContext?): Promise; -``` - -Defined in: [index.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L44) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `includeRecipeIds?` | `string`[] | -| `tenantId?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`number`\> - -##### getUserIdMapping() - -```ts -static getUserIdMapping(input): Promise< - | { - externalUserId: string; - externalUserIdInfo: string; - status: "OK"; - superTokensUserId: string; -} - | { - status: "UNKNOWN_MAPPING_ERROR"; -}>; -``` - -Defined in: [index.ts:101](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L101) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -###### Returns - -`Promise`\< - \| \{ - `externalUserId`: `string`; - `externalUserIdInfo`: `string`; - `status`: `"OK"`; - `superTokensUserId`: `string`; -\} - \| \{ - `status`: `"UNKNOWN_MAPPING_ERROR"`; -\}\> - -##### getUsersNewestFirst() - -```ts -static getUsersNewestFirst(input): Promise<{ - nextPaginationToken?: string; - users: User[]; -}>; -``` - -Defined in: [index.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L70) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `includeRecipeIds?`: `string`[]; `limit?`: `number`; `paginationToken?`: `string`; `query?`: \{ \[`key`: `string`\]: `string`; \}; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.includeRecipeIds?` | `string`[] | -| `input.limit?` | `number` | -| `input.paginationToken?` | `string` | -| `input.query?` | \{ \[`key`: `string`\]: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `nextPaginationToken?`: `string`; - `users`: [`User`](types.mdx#user)[]; -\}\> - -##### getUsersOldestFirst() - -```ts -static getUsersOldestFirst(input): Promise<{ - nextPaginationToken?: string; - users: User[]; -}>; -``` - -Defined in: [index.ts:52](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L52) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `includeRecipeIds?`: `string`[]; `limit?`: `number`; `paginationToken?`: `string`; `query?`: \{ \[`key`: `string`\]: `string`; \}; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.includeRecipeIds?` | `string`[] | -| `input.limit?` | `number` | -| `input.paginationToken?` | `string` | -| `input.query?` | \{ \[`key`: `string`\]: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `nextPaginationToken?`: `string`; - `users`: [`User`](types.mdx#user)[]; -\}\> - -##### isRecipeInitialized() - -```ts -static isRecipeInitialized(recipeId): boolean; -``` - -Defined in: [index.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L175) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeId` | `string` | - -###### Returns - -`boolean` - -##### listUsersByAccountInfo() - -```ts -static listUsersByAccountInfo( - tenantId, - accountInfo, - doUnionOfAccountInfo, -userContext?): Promise; -``` - -Defined in: [index.ts:143](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L143) - -###### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `tenantId` | `string` | `undefined` | -| `accountInfo` | [`AccountInfoInput`](recipe-accountlinking-types.mdx#accountinfoinput) | `undefined` | -| `doUnionOfAccountInfo` | `boolean` | `false` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -###### Returns - -`Promise`\<[`User`](types.mdx#user)[]\> - -##### updateOrDeleteUserIdMappingInfo() - -```ts -static updateOrDeleteUserIdMappingInfo(input): Promise<{ - status: "OK" | "UNKNOWN_MAPPING_ERROR"; -}>; -``` - -Defined in: [index.ts:124](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L124) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `externalUserIdInfo?`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.externalUserIdInfo?` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_MAPPING_ERROR"`; -\}\> - -*** - -### RecipeUserId - -Defined in: [recipeUserId.ts:1](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipeUserId.ts#L1) - -#### Constructors - -##### Constructor - -```ts -new RecipeUserId(recipeUserId): RecipeUserId; -``` - -Defined in: [recipeUserId.ts:3](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipeUserId.ts#L3) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | `string` | - -###### Returns - -[`RecipeUserId`](#recipeuserid-1) - -#### Methods - -##### getAsString() - -```ts -getAsString(): string; -``` - -Defined in: [recipeUserId.ts:10](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipeUserId.ts#L10) - -###### Returns - -`string` - -*** - -### User - -Defined in: [user.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L91) - -#### Implements - -- [`User`](types.mdx#user) - -#### Constructors - -##### Constructor - -```ts -new User(user): User; -``` - -Defined in: [user.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L109) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | `UserWithoutHelperFunctions` | - -###### Returns - -[`User`](#user-1) - -#### Properties - -| Property | Modifier | Type | Defined in | -| ------ | ------ | ------ | ------ | -| `emails` | `readonly` | `string`[] | [user.ts:96](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L96) | -| `id` | `readonly` | `string` | [user.ts:92](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L92) | -| `isPrimaryUser` | `readonly` | `boolean` | [user.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L93) | -| `loginMethods` | `readonly` | `LoginMethod`[] | [user.ts:105](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L105) | -| `phoneNumbers` | `readonly` | `string`[] | [user.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L97) | -| `tenantIds` | `readonly` | `string`[] | [user.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L94) | -| `thirdParty` | `readonly` | `object`[] | [user.ts:98](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L98) | -| `timeJoined` | `readonly` | `number` | [user.ts:107](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L107) | -| `webauthn` | `readonly` | `object` | [user.ts:102](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L102) | -| `webauthn.credentialIds` | `public` | `string`[] | [user.ts:103](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L103) | - -#### Methods - -##### toJson() - -```ts -toJson(): JSONObject; -``` - -Defined in: [user.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L133) - -###### Returns - -[`JSONObject`](types.mdx#jsonobject) - -###### Implementation of - -```ts -UserType.toJson -``` - -##### fromApi() - -```ts -static fromApi(apiUser): User; -``` - -Defined in: [user.ts:129](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/user.ts#L129) - -This function is used to create a User object from the API response. - -###### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `apiUser` | `Omit`\<`UserWithoutHelperFunctions`, `"id"`\> & `object` | The API response from the user endpoint. | - -###### Returns - -[`User`](#user-1) - -A User object. - -## Variables - -### convertToRecipeUserId() - -```ts -convertToRecipeUserId: (recipeUserId) => RecipeUserId = SuperTokensWrapper.convertToRecipeUserId; -``` - -Defined in: [index.ts:206](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L206) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | `string` | - -#### Returns - -[`RecipeUserId`](#recipeuserid-1) - -*** - -### createUserIdMapping() - -```ts -createUserIdMapping: (input) => Promise< - | { - status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; -} - | { - doesExternalUserIdExist: boolean; - doesSuperTokensUserIdExist: boolean; - status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR"; -}> = SuperTokensWrapper.createUserIdMapping; -``` - -Defined in: [index.ts:194](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L194) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `externalUserId`: `string`; `externalUserIdInfo?`: `string`; `force?`: `boolean`; `superTokensUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.externalUserId` | `string` | -| `input.externalUserIdInfo?` | `string` | -| `input.force?` | `boolean` | -| `input.superTokensUserId` | `string` | -| `input.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() - -```ts -deleteUser: (userId, removeAllLinkedAccounts, userContext?) => Promise<{ - status: "OK"; -}> = SuperTokensWrapper.deleteUser; -``` - -Defined in: [index.ts:192](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L192) - -#### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `userId` | `string` | `undefined` | -| `removeAllLinkedAccounts` | `boolean` | `true` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### deleteUserIdMapping() - -```ts -deleteUserIdMapping: (input) => Promise<{ - didMappingExist: boolean; - status: "OK"; -}> = SuperTokensWrapper.deleteUserIdMapping; -``` - -Defined in: [index.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L198) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `force?`: `boolean`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.force?` | `boolean` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -#### Returns - -`Promise`\<\{ - `didMappingExist`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### Error - -```ts -Error: typeof default = SuperTokensWrapper.Error; -``` - -Defined in: [index.ts:212](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L212) - -*** - -### getAllCORSHeaders() - -```ts -getAllCORSHeaders: () => string[] = SuperTokensWrapper.getAllCORSHeaders; -``` - -Defined in: [index.ts:184](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L184) - -#### Returns - -`string`[] - -*** - -### getRequestFromUserContext() - -```ts -getRequestFromUserContext: (userContext) => BaseRequest = SuperTokensWrapper.getRequestFromUserContext; -``` - -Defined in: [index.ts:208](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L208) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`BaseRequest` - -*** - -### getUser() - -```ts -getUser: (userId, userContext?) => Promise = SuperTokensWrapper.getUser; -``` - -Defined in: [index.ts:202](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L202) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -*** - -### getUserCount() - -```ts -getUserCount: (includeRecipeIds?, tenantId?, userContext?) => Promise = SuperTokensWrapper.getUserCount; -``` - -Defined in: [index.ts:186](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L186) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `includeRecipeIds?` | `string`[] | -| `tenantId?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`number`\> - -*** - -### getUserIdMapping() - -```ts -getUserIdMapping: (input) => Promise< - | { - externalUserId: string; - externalUserIdInfo: string; - status: "OK"; - superTokensUserId: string; -} - | { - status: "UNKNOWN_MAPPING_ERROR"; -}> = SuperTokensWrapper.getUserIdMapping; -``` - -Defined in: [index.ts:196](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L196) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -#### Returns - -`Promise`\< - \| \{ - `externalUserId`: `string`; - `externalUserIdInfo`: `string`; - `status`: `"OK"`; - `superTokensUserId`: `string`; -\} - \| \{ - `status`: `"UNKNOWN_MAPPING_ERROR"`; -\}\> - -*** - -### getUsersNewestFirst() - -```ts -getUsersNewestFirst: (input) => Promise<{ - nextPaginationToken?: string; - users: User[]; -}> = SuperTokensWrapper.getUsersNewestFirst; -``` - -Defined in: [index.ts:190](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L190) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `includeRecipeIds?`: `string`[]; `limit?`: `number`; `paginationToken?`: `string`; `query?`: \{ \[`key`: `string`\]: `string`; \}; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.includeRecipeIds?` | `string`[] | -| `input.limit?` | `number` | -| `input.paginationToken?` | `string` | -| `input.query?` | \{ \[`key`: `string`\]: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `nextPaginationToken?`: `string`; - `users`: [`User`](types.mdx#user)[]; -\}\> - -*** - -### getUsersOldestFirst() - -```ts -getUsersOldestFirst: (input) => Promise<{ - nextPaginationToken?: string; - users: User[]; -}> = SuperTokensWrapper.getUsersOldestFirst; -``` - -Defined in: [index.ts:188](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L188) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `includeRecipeIds?`: `string`[]; `limit?`: `number`; `paginationToken?`: `string`; `query?`: \{ \[`key`: `string`\]: `string`; \}; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.includeRecipeIds?` | `string`[] | -| `input.limit?` | `number` | -| `input.paginationToken?` | `string` | -| `input.query?` | \{ \[`key`: `string`\]: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `nextPaginationToken?`: `string`; - `users`: [`User`](types.mdx#user)[]; -\}\> - -*** - -### init() - -```ts -init: (config) => void = SuperTokensWrapper.init; -``` - -Defined in: [index.ts:182](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L182) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config` | [`TypeInput`](types.mdx#typeinput) | - -#### Returns - -`void` - -*** - -### isRecipeInitialized() - -```ts -isRecipeInitialized: (recipeId) => boolean = SuperTokensWrapper.isRecipeInitialized; -``` - -Defined in: [index.ts:210](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L210) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeId` | `string` | - -#### Returns - -`boolean` - -*** - -### listUsersByAccountInfo() - -```ts -listUsersByAccountInfo: (tenantId, accountInfo, doUnionOfAccountInfo, userContext?) => Promise = SuperTokensWrapper.listUsersByAccountInfo; -``` - -Defined in: [index.ts:204](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L204) - -#### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `tenantId` | `string` | `undefined` | -| `accountInfo` | [`AccountInfoInput`](recipe-accountlinking-types.mdx#accountinfoinput) | `undefined` | -| `doUnionOfAccountInfo` | `boolean` | `false` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -#### Returns - -`Promise`\<[`User`](types.mdx#user)[]\> - -*** - -### updateOrDeleteUserIdMappingInfo() - -```ts -updateOrDeleteUserIdMappingInfo: (input) => Promise<{ - status: "OK" | "UNKNOWN_MAPPING_ERROR"; -}> = SuperTokensWrapper.updateOrDeleteUserIdMappingInfo; -``` - -Defined in: [index.ts:200](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/index.ts#L200) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `externalUserIdInfo?`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; `userIdType?`: `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"`; \} | -| `input.externalUserIdInfo?` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | -| `input.userIdType?` | `"SUPERTOKENS"` \| `"EXTERNAL"` \| `"ANY"` | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_MAPPING_ERROR"`; -\}\> - -## References - -### SuperTokensConfig - -Renames and re-exports [TypeInput](types.mdx#typeinput) - -*** - -### SuperTokensPlugin - -Re-exports [SuperTokensPlugin](types.mdx#supertokensplugin) - -*** - -### SuperTokensPublicConfig - -Re-exports [SuperTokensPublicConfig](types.mdx#supertokenspublicconfig) - -*** - -### SuperTokensPublicPlugin - -Re-exports [SuperTokensPublicPlugin](types.mdx#supertokenspublicplugin) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking-types.mdx deleted file mode 100644 index 29680a2225..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking-types.mdx +++ /dev/null @@ -1,148 +0,0 @@ ---- -title: supertokens-node/recipe/accountlinking/types -page_title: Account Linking Types -sidebar_position: 8 -description: References documentation for the supertokens-node/recipe/accountlinking/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Account Linking Types - - -## Type Aliases - -### AccountInfo - -```ts -type AccountInfo = object; -``` - -Defined in: [recipe/accountlinking/types.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L175) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `email?` | `string` | [recipe/accountlinking/types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L176) | -| `phoneNumber?` | `string` | [recipe/accountlinking/types.ts:177](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L177) | -| `thirdParty?` | `object` | [recipe/accountlinking/types.ts:178](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L178) | -| `thirdParty.id` | `string` | [recipe/accountlinking/types.ts:179](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L179) | -| `thirdParty.userId` | `string` | [recipe/accountlinking/types.ts:180](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L180) | -| `webauthn?` | `object` | [recipe/accountlinking/types.ts:182](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L182) | -| `webauthn.credentialIds` | `string`[] | [recipe/accountlinking/types.ts:183](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L183) | - -*** - -### AccountInfoInput - -```ts -type AccountInfoInput = Omit & object; -``` - -Defined in: [recipe/accountlinking/types.ts:187](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L187) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `webauthn?` | `object` | [recipe/accountlinking/types.ts:188](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L188) | -| `webauthn.credentialId` | `string` | [recipe/accountlinking/types.ts:189](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L189) | - -*** - -### AccountInfoWithRecipeId - -```ts -type AccountInfoWithRecipeId = object & AccountInfo; -``` - -Defined in: [recipe/accountlinking/types.ts:193](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L193) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `recipeId` | `"emailpassword"` \| `"thirdparty"` \| `"passwordless"` \| `"webauthn"` | [recipe/accountlinking/types.ts:194](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L194) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/accountlinking/types.ts:71](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L71) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `canCreatePrimaryUser` | (`input`) => `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"`; \}\> | [recipe/accountlinking/types.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L84) | -| `canLinkAccounts` | (`input`) => `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"`; \}\> | [recipe/accountlinking/types.ts:113](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L113) | -| `createPrimaryUser` | (`input`) => `Promise`\< \| \{ `status`: `"OK"`; `user`: [`User`](types.mdx#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"`; \}\> | [recipe/accountlinking/types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L97) | -| `deleteUser` | (`input`) => `Promise`\<\{ `status`: `"OK"`; \}\> | [recipe/accountlinking/types.ts:168](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L168) | -| `getUser` | (`input`) => `Promise`\<[`User`](types.mdx#user) \| `undefined`\> | [recipe/accountlinking/types.ts:161](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L161) | -| `getUsers` | (`input`) => `Promise`\<\{ `nextPaginationToken?`: `string`; `users`: [`User`](types.mdx#user)[]; \}\> | [recipe/accountlinking/types.ts:72](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L72) | -| `linkAccounts` | (`input`) => `Promise`\< \| \{ `accountsAlreadyLinked`: `boolean`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| \{ `primaryUserId`: `string`; `status`: `"RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"`; `user`: [`User`](types.mdx#user); \} \| \{ `description`: `string`; `primaryUserId`: `string`; `status`: `"ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"`; \} \| \{ `status`: `"INPUT_USER_IS_NOT_A_PRIMARY_USER"`; \}\> | [recipe/accountlinking/types.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L136) | -| `listUsersByAccountInfo` | (`input`) => `Promise`\<[`User`](types.mdx#user)[]\> | [recipe/accountlinking/types.ts:162](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L162) | -| `unlinkAccount` | (`input`) => `Promise`\<\{ `status`: `"OK"`; `wasLinked`: `boolean`; `wasRecipeUserDeleted`: `boolean`; \}\> | [recipe/accountlinking/types.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L156) | - -*** - -### RecipeLevelUser - -```ts -type RecipeLevelUser = object & AccountInfoWithRecipeId; -``` - -Defined in: [recipe/accountlinking/types.ts:197](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L197) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | [recipe/accountlinking/types.ts:200](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L200) | -| `tenantIds` | `string`[] | [recipe/accountlinking/types.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L198) | -| `timeJoined` | `number` | [recipe/accountlinking/types.ts:199](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L199) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/accountlinking/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L21) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `onAccountLinked?` | (`user`, `newAccountInfo`, `userContext`) => `Promise`\<`void`\> | [recipe/accountlinking/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L22) | -| `override?` | `object` | [recipe/accountlinking/types.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L38) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/accountlinking/types.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L39) | -| `shouldDoAutomaticAccountLinking?` | (`newAccountInfo`, `user`, `session`, `tenantId`, `userContext`) => `Promise`\< \| \{ `shouldAutomaticallyLink`: `false`; \} \| \{ `shouldAutomaticallyLink`: `true`; `shouldRequireVerification`: `boolean`; \}\> | [recipe/accountlinking/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L23) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/accountlinking/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L46) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `onAccountLinked` | (`user`, `newAccountInfo`, `userContext`) => `Promise`\<`void`\> | [recipe/accountlinking/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L47) | -| `override` | `object` | [recipe/accountlinking/types.ts:63](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L63) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/accountlinking/types.ts:64](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L64) | -| `shouldDoAutomaticAccountLinking` | (`newAccountInfo`, `user`, `session`, `tenantId`, `userContext`) => `Promise`\< \| \{ `shouldAutomaticallyLink`: `false`; \} \| \{ `shouldAutomaticallyLink`: `true`; `shouldRequireVerification`: `boolean`; \}\> | [recipe/accountlinking/types.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/types.ts#L48) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking.mdx deleted file mode 100644 index 904c5180ad..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-accountlinking.mdx +++ /dev/null @@ -1,802 +0,0 @@ ---- -title: supertokens-node/recipe/accountlinking -page_title: Account Linking -sidebar_position: 8 -description: References documentation for the supertokens-node/recipe/accountlinking package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Account Linking - - -## Classes - -### default - -Defined in: [recipe/accountlinking/index.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L23) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/accountlinking/index.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L24) | - -#### Methods - -##### canCreatePrimaryUser() - -```ts -static canCreatePrimaryUser(recipeUserId, userContext?): Promise< - | { - status: "OK"; - wasAlreadyAPrimaryUser: boolean; -} - | { - description: string; - primaryUserId: string; - status: | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" - | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/accountlinking/index.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L84) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `wasAlreadyAPrimaryUser`: `boolean`; -\} - \| \{ - `description`: `string`; - `primaryUserId`: `string`; - `status`: \| `"ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"` - \| `"RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"`; -\}\> - -##### canLinkAccounts() - -```ts -static canLinkAccounts( - recipeUserId, - primaryUserId, - 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"; -}>; -``` - -Defined in: [recipe/accountlinking/index.ts:98](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L98) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `primaryUserId` | `string` | -| `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() - -```ts -static createPrimaryUser(recipeUserId, 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"; -}>; -``` - -Defined in: [recipe/accountlinking/index.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L91) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -static createPrimaryUserIdOrLinkAccounts( - tenantId, - recipeUserId, - session?, -userContext?): Promise; -``` - -Defined in: [recipe/accountlinking/index.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L35) - -This is a function which is a combination of createPrimaryUser and -linkAccounts where the input recipeUserId is either linked to a user that it can be -linked to, or is made into a primary user. - -The output will be the user ID of the user that it was linked to, or it will be the -same as the input recipeUserId if it was made into a primary user, or if there was -no linking that happened. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -##### getPrimaryUserThatCanBeLinkedToRecipeUserId() - -```ts -static getPrimaryUserThatCanBeLinkedToRecipeUserId( - tenantId, - recipeUserId, -userContext?): Promise; -``` - -Defined in: [recipe/accountlinking/index.ts:67](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L67) - -This function returns the primary user that the input recipe ID can be -linked to. It can be used to determine which primary account the linking -will happen to if the input recipe user ID was to be linked. - -If the function returns undefined, it means that there is no primary user -that the input recipe ID can be linked to, and therefore it can be made -into a primary user itself. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -##### isEmailChangeAllowed() - -```ts -static isEmailChangeAllowed( - recipeUserId, - newEmail, - isVerified, - session?, -userContext?): Promise; -``` - -Defined in: [recipe/accountlinking/index.ts:159](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L159) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `newEmail` | `string` | -| `isVerified` | `boolean` | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`boolean`\> - -##### isSignInAllowed() - -```ts -static isSignInAllowed( - tenantId, - recipeUserId, - session?, -userContext?): Promise; -``` - -Defined in: [recipe/accountlinking/index.ts:137](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L137) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`boolean`\> - -##### isSignUpAllowed() - -```ts -static isSignUpAllowed( - tenantId, - newUser, - isVerified, - session?, -userContext?): Promise; -``` - -Defined in: [recipe/accountlinking/index.ts:121](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L121) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `newUser` | [`AccountInfoWithRecipeId`](recipe-accountlinking-types.mdx#accountinfowithrecipeid) | -| `isVerified` | `boolean` | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`boolean`\> - -##### linkAccounts() - -```ts -static linkAccounts( - recipeUserId, - primaryUserId, - 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"; -}>; -``` - -Defined in: [recipe/accountlinking/index.ts:106](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L106) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `primaryUserId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `accountsAlreadyLinked`: `boolean`; - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `primaryUserId`: `string`; - `status`: `"RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"`; - `user`: [`User`](types.mdx#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() - -```ts -static unlinkAccount(recipeUserId, userContext?): Promise<{ - status: "OK"; - wasLinked: boolean; - wasRecipeUserDeleted: boolean; -}>; -``` - -Defined in: [recipe/accountlinking/index.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L114) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `wasLinked`: `boolean`; - `wasRecipeUserDeleted`: `boolean`; -\}\> - -## Variables - -### canCreatePrimaryUser() - -```ts -const canCreatePrimaryUser: (recipeUserId, userContext?) => Promise< - | { - status: "OK"; - wasAlreadyAPrimaryUser: boolean; -} - | { - description: string; - primaryUserId: string; - status: | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" - | "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; -}> = Wrapper.canCreatePrimaryUser; -``` - -Defined in: [recipe/accountlinking/index.ts:184](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L184) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `wasAlreadyAPrimaryUser`: `boolean`; -\} - \| \{ - `description`: `string`; - `primaryUserId`: `string`; - `status`: \| `"ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"` - \| `"RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"`; -\}\> - -*** - -### canLinkAccounts() - -```ts -const canLinkAccounts: (recipeUserId, primaryUserId, 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"; -}> = Wrapper.canLinkAccounts; -``` - -Defined in: [recipe/accountlinking/index.ts:186](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L186) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `primaryUserId` | `string` | -| `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() - -```ts -const createPrimaryUser: (recipeUserId, 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"; -}> = Wrapper.createPrimaryUser; -``` - -Defined in: [recipe/accountlinking/index.ts:185](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L185) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -const createPrimaryUserIdOrLinkAccounts: (tenantId, recipeUserId, session?, userContext?) => Promise = Wrapper.createPrimaryUserIdOrLinkAccounts; -``` - -Defined in: [recipe/accountlinking/index.ts:189](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L189) - -This is a function which is a combination of createPrimaryUser and -linkAccounts where the input recipeUserId is either linked to a user that it can be -linked to, or is made into a primary user. - -The output will be the user ID of the user that it was linked to, or it will be the -same as the input recipeUserId if it was made into a primary user, or if there was -no linking that happened. - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -*** - -### getPrimaryUserThatCanBeLinkedToRecipeUserId() - -```ts -const getPrimaryUserThatCanBeLinkedToRecipeUserId: (tenantId, recipeUserId, userContext?) => Promise = Wrapper.getPrimaryUserThatCanBeLinkedToRecipeUserId; -``` - -Defined in: [recipe/accountlinking/index.ts:190](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L190) - -This function returns the primary user that the input recipe ID can be -linked to. It can be used to determine which primary account the linking -will happen to if the input recipe user ID was to be linked. - -If the function returns undefined, it means that there is no primary user -that the input recipe ID can be linked to, and therefore it can be made -into a primary user itself. - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`User`](types.mdx#user)\> - -*** - -### init() - -```ts -const init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/accountlinking/index.ts:183](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L183) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-accountlinking-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### isEmailChangeAllowed() - -```ts -const isEmailChangeAllowed: (recipeUserId, newEmail, isVerified, session?, userContext?) => Promise = Wrapper.isEmailChangeAllowed; -``` - -Defined in: [recipe/accountlinking/index.ts:193](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L193) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `newEmail` | `string` | -| `isVerified` | `boolean` | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`boolean`\> - -*** - -### isSignInAllowed() - -```ts -const isSignInAllowed: (tenantId, recipeUserId, session?, userContext?) => Promise = Wrapper.isSignInAllowed; -``` - -Defined in: [recipe/accountlinking/index.ts:192](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L192) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`boolean`\> - -*** - -### isSignUpAllowed() - -```ts -const isSignUpAllowed: (tenantId, newUser, isVerified, session?, userContext?) => Promise = Wrapper.isSignUpAllowed; -``` - -Defined in: [recipe/accountlinking/index.ts:191](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L191) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `newUser` | [`AccountInfoWithRecipeId`](recipe-accountlinking-types.mdx#accountinfowithrecipeid) | -| `isVerified` | `boolean` | -| `session?` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`boolean`\> - -*** - -### linkAccounts() - -```ts -const linkAccounts: (recipeUserId, primaryUserId, 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"; -}> = Wrapper.linkAccounts; -``` - -Defined in: [recipe/accountlinking/index.ts:187](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L187) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `primaryUserId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `accountsAlreadyLinked`: `boolean`; - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `primaryUserId`: `string`; - `status`: `"RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"`; - `user`: [`User`](types.mdx#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() - -```ts -const unlinkAccount: (recipeUserId, userContext?) => Promise<{ - status: "OK"; - wasLinked: boolean; - wasRecipeUserDeleted: boolean; -}> = Wrapper.unlinkAccount; -``` - -Defined in: [recipe/accountlinking/index.ts:188](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/accountlinking/index.ts#L188) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `wasLinked`: `boolean`; - `wasRecipeUserDeleted`: `boolean`; -\}\> - -## References - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-accountlinking-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard-types.mdx deleted file mode 100644 index 7b648a7304..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard-types.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: supertokens-node/recipe/dashboard/types -page_title: Dashboard Types -sidebar_position: 11 -description: References documentation for the supertokens-node/recipe/dashboard/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Dashboard Types - - -## Type Aliases - -### APIFunction() - -```ts -type APIFunction = (apiImplementation, tenantId, options, userContext) => Promise; -``` - -Defined in: [recipe/dashboard/types.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L68) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `apiImplementation` | [`APIInterface`](#apiinterface) | -| `tenantId` | `string` | -| `options` | [`APIOptions`](#apioptions) | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`any`\> - -*** - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/dashboard/types.ts:64](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L64) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `dashboardGET` | `undefined` \| (`input`) => `Promise`\<`string`\> | [recipe/dashboard/types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L65) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/dashboard/types.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L54) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/dashboard/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L61) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/dashboard/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L56) | -| `isInServerlessEnv` | `boolean` | [recipe/dashboard/types.ts:60](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L60) | -| `recipeId` | `string` | [recipe/dashboard/types.ts:57](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L57) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/dashboard/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L55) | -| `req` | `BaseRequest` | [recipe/dashboard/types.ts:58](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L58) | -| `res` | `BaseResponse` | [recipe/dashboard/types.ts:59](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L59) | - -*** - -### AuthMode - -```ts -type AuthMode = "api-key" | "email-password"; -``` - -Defined in: [recipe/dashboard/types.ts:77](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L77) - -*** - -### CoreConfigFieldInfo - -```ts -type CoreConfigFieldInfo = object; -``` - -Defined in: [recipe/dashboard/types.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L84) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `defaultValue` | `string` \| `number` \| `boolean` \| `null` | [recipe/dashboard/types.ts:92](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L92) | -| `description` | `string` | [recipe/dashboard/types.ts:88](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L88) | -| `isDifferentAcrossTenants` | `boolean` | [recipe/dashboard/types.ts:89](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L89) | -| `isNullable` | `boolean` | [recipe/dashboard/types.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L91) | -| `isPluginProperty` | `boolean` | [recipe/dashboard/types.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L93) | -| `isPluginPropertyEditable` | `boolean` | [recipe/dashboard/types.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L94) | -| `key` | `string` | [recipe/dashboard/types.ts:85](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L85) | -| `possibleValues?` | `string`[] | [recipe/dashboard/types.ts:90](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L90) | -| `value` | `string` \| `number` \| `boolean` \| `null` | [recipe/dashboard/types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L87) | -| `valueType` | `"string"` \| `"boolean"` \| `"number"` | [recipe/dashboard/types.ts:86](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L86) | - -*** - -### RecipeIdForUser - -```ts -type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless" | "webauthn"; -``` - -Defined in: [recipe/dashboard/types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L75) - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/dashboard/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L45) - -#### Methods - -##### getDashboardBundleLocation() - -```ts -getDashboardBundleLocation(input): Promise; -``` - -Defined in: [recipe/dashboard/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L46) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<`string`\> - -##### shouldAllowAccess() - -```ts -shouldAllowAccess(input): Promise; -``` - -Defined in: [recipe/dashboard/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L47) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `config`: [`TypeNormalisedInput`](#typenormalisedinput); `req`: `BaseRequest`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.config` | [`TypeNormalisedInput`](#typenormalisedinput) | -| `input.req` | `BaseRequest` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<`boolean`\> - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/dashboard/types.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L20) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `admins?` | `string`[] | [recipe/dashboard/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L22) | -| `apiKey?` | `string` | [recipe/dashboard/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L21) | -| `override?` | `object` | [recipe/dashboard/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L23) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/dashboard/types.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L28) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/dashboard/types.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L24) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/dashboard/types.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L32) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `admins?` | `string`[] | [recipe/dashboard/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L34) | -| `apiKey?` | `string` | [recipe/dashboard/types.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L33) | -| `authMode` | [`AuthMode`](#authmode) | [recipe/dashboard/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L35) | -| `override` | `object` | [recipe/dashboard/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L36) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/dashboard/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L41) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/dashboard/types.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L37) | - -*** - -### UserWithFirstAndLastName - -```ts -type UserWithFirstAndLastName = User & object; -``` - -Defined in: [recipe/dashboard/types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L79) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `firstName?` | `string` | [recipe/dashboard/types.ts:80](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L80) | -| `lastName?` | `string` | [recipe/dashboard/types.ts:81](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/types.ts#L81) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard.mdx deleted file mode 100644 index 1d92c2a773..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-dashboard.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: supertokens-node/recipe/dashboard -page_title: Dashboard -sidebar_position: 11 -description: References documentation for the supertokens-node/recipe/dashboard package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Dashboard - - -## Classes - -### default - -Defined in: [recipe/dashboard/index.ts:18](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/index.ts#L18) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/dashboard/index.ts:19](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/index.ts#L19) | - -## Variables - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/dashboard/index.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/dashboard/index.ts#L22) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-dashboard-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-dashboard-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-dashboard-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-dashboard-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword-types.mdx deleted file mode 100644 index dcb0008ffb..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword-types.mdx +++ /dev/null @@ -1,567 +0,0 @@ ---- -title: supertokens-node/recipe/emailpassword/types -page_title: EmailPassword Types -sidebar_position: 2 -description: References documentation for the supertokens-node/recipe/emailpassword/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# EmailPassword Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/emailpassword/types.ts:208](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L208) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailExistsGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `exists`: `boolean`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailpassword/types.ts:209](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L209) | -| `generatePasswordResetTokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| \{ `reason`: `string`; `status`: `"PASSWORD_RESET_NOT_ALLOWED"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailpassword/types.ts:219](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L219) | -| `passwordResetPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `email`: `string`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| \{ `status`: `"RESET_PASSWORD_INVALID_TOKEN_ERROR"`; \} \| \{ `failureReason`: `string`; `status`: `"PASSWORD_POLICY_VIOLATED_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailpassword/types.ts:240](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L240) | -| `signInPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| \{ `reason`: `string`; `status`: `"SIGN_IN_NOT_ALLOWED"`; \} \| \{ `status`: `"WRONG_CREDENTIALS_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailpassword/types.ts:264](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L264) | -| `signUpPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| \{ `reason`: `string`; `status`: `"SIGN_UP_NOT_ALLOWED"`; \} \| \{ `status`: `"EMAIL_ALREADY_EXISTS_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailpassword/types.ts:292](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L292) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/emailpassword/types.ts:197](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L197) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/emailpassword/types.ts:199](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L199) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/emailpassword/types.ts:200](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L200) | -| `emailDelivery` | `EmailDeliveryIngredient`\<[`TypeEmailPasswordEmailDeliveryInput`](#typeemailpasswordemaildeliveryinput)\> | [recipe/emailpassword/types.ts:205](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L205) | -| `isInServerlessEnv` | `boolean` | [recipe/emailpassword/types.ts:202](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L202) | -| `recipeId` | `string` | [recipe/emailpassword/types.ts:201](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L201) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/emailpassword/types.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L198) | -| `req` | `BaseRequest` | [recipe/emailpassword/types.ts:203](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L203) | -| `res` | `BaseResponse` | [recipe/emailpassword/types.ts:204](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L204) | - -*** - -### NormalisedFormField - -```ts -type NormalisedFormField = object; -``` - -Defined in: [recipe/emailpassword/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L55) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `id` | `string` | [recipe/emailpassword/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L56) | -| `optional` | `boolean` | [recipe/emailpassword/types.ts:58](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L58) | -| `validate` | (`value`, `tenantId`, `userContext`) => `Promise`\<`string` \| `undefined`\> | [recipe/emailpassword/types.ts:57](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L57) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/emailpassword/types.ts:86](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L86) - -#### Methods - -##### consumePasswordResetToken() - -```ts -consumePasswordResetToken(input): Promise< - | { - email: string; - status: "OK"; - userId: string; -} - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L167) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| \{ - `status`: `"RESET_PASSWORD_INVALID_TOKEN_ERROR"`; -\}\> - -##### createNewRecipeUser() - -```ts -createNewRecipeUser(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L114) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `password`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.password` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"EMAIL_ALREADY_EXISTS_ERROR"`; -\}\> - -##### createResetPasswordToken() - -```ts -createResetPasswordToken(input): Promise< - | { - status: "OK"; - token: string; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:160](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L160) - -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 - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `userId`: `string`; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.userId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -##### signIn() - -```ts -signIn(input): 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"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:128](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L128) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `password`: `string`; `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.password` | `string` | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -signUp(input): 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"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L87) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `password`: `string`; `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.password` | `string` | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -updateEmailOrPassword(input): 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"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L176) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `applyPasswordPolicy?`: `boolean`; `email?`: `string`; `password?`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `tenantIdForPasswordPolicy`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.applyPasswordPolicy?` | `boolean` | -| `input.email?` | `string` | -| `input.password?` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.tenantIdForPasswordPolicy` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#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() - -```ts -verifyCredentials(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "WRONG_CREDENTIALS_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/types.ts:148](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L148) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `password`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.password` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"WRONG_CREDENTIALS_ERROR"`; -\}\> - -*** - -### TypeEmailPasswordEmailDeliveryInput - -```ts -type TypeEmailPasswordEmailDeliveryInput = TypeEmailPasswordPasswordResetEmailDeliveryInput; -``` - -Defined in: [recipe/emailpassword/types.ts:332](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L332) - -*** - -### TypeEmailPasswordPasswordResetEmailDeliveryInput - -```ts -type TypeEmailPasswordPasswordResetEmailDeliveryInput = object; -``` - -Defined in: [recipe/emailpassword/types.ts:321](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L321) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `passwordResetLink` | `string` | [recipe/emailpassword/types.ts:328](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L328) | -| `tenantId` | `string` | [recipe/emailpassword/types.ts:329](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L329) | -| `type` | `"PASSWORD_RESET"` | [recipe/emailpassword/types.ts:322](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L322) | -| `user` | `object` | [recipe/emailpassword/types.ts:323](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L323) | -| `user.email` | `string` | [recipe/emailpassword/types.ts:326](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L326) | -| `user.id` | `string` | [recipe/emailpassword/types.ts:324](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L324) | -| `user.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) \| `undefined` | [recipe/emailpassword/types.ts:325](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L325) | - -*** - -### TypeFormField - -```ts -type TypeFormField = object; -``` - -Defined in: [recipe/emailpassword/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L49) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `id` | `string` | [recipe/emailpassword/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L49) | -| `value` | `any` | [recipe/emailpassword/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L49) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/emailpassword/types.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L74) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailDelivery?` | `EmailDeliveryTypeInput`\<[`TypeEmailPasswordEmailDeliveryInput`](#typeemailpasswordemaildeliveryinput)\> | [recipe/emailpassword/types.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L76) | -| `override?` | `object` | [recipe/emailpassword/types.ts:77](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L77) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/emailpassword/types.ts:82](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L82) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/emailpassword/types.ts:78](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L78) | -| `signUpFeature?` | [`TypeInputSignUp`](#typeinputsignup) | [recipe/emailpassword/types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L75) | - -*** - -### TypeInputFormField - -```ts -type TypeInputFormField = object; -``` - -Defined in: [recipe/emailpassword/types.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L43) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `id` | `string` | [recipe/emailpassword/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L44) | -| `optional?` | `boolean` | [recipe/emailpassword/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L46) | -| `validate?` | (`value`, `tenantId`, `userContext`) => `Promise`\<`string` \| `undefined`\> | [recipe/emailpassword/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L45) | - -*** - -### TypeInputSignUp - -```ts -type TypeInputSignUp = object; -``` - -Defined in: [recipe/emailpassword/types.ts:51](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L51) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `formFields?` | [`TypeInputFormField`](#typeinputformfield)[] | [recipe/emailpassword/types.ts:52](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L52) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/emailpassword/types.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L27) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getEmailDeliveryConfig` | (`isInServerlessEnv`) => `EmailDeliveryTypeInputWithService`\<[`TypeEmailPasswordEmailDeliveryInput`](#typeemailpasswordemaildeliveryinput)\> | [recipe/emailpassword/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L30) | -| `override` | `object` | [recipe/emailpassword/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L34) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/emailpassword/types.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L39) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/emailpassword/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L35) | -| `resetPasswordUsingTokenFeature` | [`TypeNormalisedInputResetPasswordUsingTokenFeature`](#typenormalisedinputresetpasswordusingtokenfeature-1) | [recipe/emailpassword/types.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L33) | -| `signInFeature` | [`TypeNormalisedInputSignIn`](#typenormalisedinputsignin) | [recipe/emailpassword/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L29) | -| `signUpFeature` | [`TypeNormalisedInputSignUp`](#typenormalisedinputsignup) | [recipe/emailpassword/types.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L28) | - -*** - -### TypeNormalisedInputResetPasswordUsingTokenFeature - -```ts -type TypeNormalisedInputResetPasswordUsingTokenFeature = object; -``` - -Defined in: [recipe/emailpassword/types.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L69) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `formFieldsForGenerateTokenForm` | [`NormalisedFormField`](#normalisedformfield)[] | [recipe/emailpassword/types.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L70) | -| `formFieldsForPasswordResetForm` | [`NormalisedFormField`](#normalisedformfield)[] | [recipe/emailpassword/types.ts:71](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L71) | - -*** - -### TypeNormalisedInputSignIn - -```ts -type TypeNormalisedInputSignIn = object; -``` - -Defined in: [recipe/emailpassword/types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L65) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `formFields` | [`NormalisedFormField`](#normalisedformfield)[] | [recipe/emailpassword/types.ts:66](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L66) | - -*** - -### TypeNormalisedInputSignUp - -```ts -type TypeNormalisedInputSignUp = object; -``` - -Defined in: [recipe/emailpassword/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L61) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `formFields` | [`NormalisedFormField`](#normalisedformfield)[] | [recipe/emailpassword/types.ts:62](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/types.ts#L62) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword.mdx deleted file mode 100644 index 9e887c86ea..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailpassword.mdx +++ /dev/null @@ -1,1125 +0,0 @@ ---- -title: supertokens-node/recipe/emailpassword -page_title: EmailPassword -sidebar_position: 2 -description: References documentation for the supertokens-node/recipe/emailpassword package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# EmailPassword - - -## Classes - -### default - -Defined in: [recipe/emailpassword/index.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L27) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [recipe/emailpassword/index.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L30) | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/emailpassword/index.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L28) | - -#### Methods - -##### consumePasswordResetToken() - -```ts -static consumePasswordResetToken( - tenantId, - token, - userContext?): Promise< - | { - email: string; - status: "OK"; - userId: string; -} - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:236](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L236) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `token` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| \{ - `status`: `"RESET_PASSWORD_INVALID_TOKEN_ERROR"`; -\}\> - -##### createResetPasswordLink() - -```ts -static createResetPasswordLink( - tenantId, - userId, - email, - userContext?): Promise< - | { - link: string; - status: "OK"; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:280](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L280) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -##### createResetPasswordToken() - -```ts -static createResetPasswordToken( - tenantId, - userId, - email, - userContext?): Promise< - | { - status: "OK"; - token: string; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:183](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L183) - -We do not make email optional here cause we want to -allow passing in primaryUserId. If we make email optional, -and if the user provides a primaryUserId, then it may result in two problems: - - there is no recipeUserId = input primaryUserId, in this case, - this function will throw an error - - There is a recipe userId = input primaryUserId, but that recipe has no email, - or has wrong email compared to what the user wanted to generate a reset token for. - -And we want to allow primaryUserId being passed in. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -##### resetPasswordUsingToken() - -```ts -static resetPasswordUsingToken( - tenantId, - token, - newPassword, - userContext?): Promise< - | { - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR"; -} - | { - failureReason: string; - status: "PASSWORD_POLICY_VIOLATED_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:197](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L197) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `token` | `string` | -| `newPassword` | `string` | -| `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() - -```ts -static sendEmail(input): Promise; -``` - -Defined in: [recipe/emailpassword/index.ts:343](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L343) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeEmailPasswordPasswordResetEmailDeliveryInput`](recipe-emailpassword-types.mdx#typeemailpasswordpasswordresetemaildeliveryinput) & `object` | - -###### Returns - -`Promise`\<`void`\> - -##### sendResetPasswordEmail() - -```ts -static sendResetPasswordEmail( - tenantId, - userId, - email, - userContext?): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:305](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L305) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -##### signIn() - -###### Call Signature - -```ts -static signIn( - tenantId, - email, - password, - session?, - userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "WRONG_CREDENTIALS_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L100) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"WRONG_CREDENTIALS_ERROR"`; -\}\> - -###### Call Signature - -```ts -static signIn( - tenantId, - email, - password, - session, - 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"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:107](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L107) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -###### Call Signature - -```ts -static signUp( - tenantId, - email, - password, - session?, - userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L32) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"EMAIL_ALREADY_EXISTS_ERROR"`; -\}\> - -###### Call Signature - -```ts -static signUp( - tenantId, - email, - password, - session, - 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"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L46) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -static updateEmailOrPassword(input): 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"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:255](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L255) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `applyPasswordPolicy?`: `boolean`; `email?`: `string`; `password?`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `tenantIdForPasswordPolicy?`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.applyPasswordPolicy?` | `boolean` | -| `input.email?` | `string` | -| `input.password?` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.tenantIdForPasswordPolicy?` | `string` | -| `input.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() - -```ts -static verifyCredentials( - tenantId, - email, - password, - userContext?): Promise<{ - status: "OK" | "WRONG_CREDENTIALS_ERROR"; -}>; -``` - -Defined in: [recipe/emailpassword/index.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L153) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"WRONG_CREDENTIALS_ERROR"`; -\}\> - -## Variables - -### consumePasswordResetToken() - -```ts -consumePasswordResetToken: (tenantId, token, userContext?) => Promise< - | { - email: string; - status: "OK"; - userId: string; -} - | { - status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; -}> = Wrapper.consumePasswordResetToken; -``` - -Defined in: [recipe/emailpassword/index.ts:369](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L369) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `token` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| \{ - `status`: `"RESET_PASSWORD_INVALID_TOKEN_ERROR"`; -\}\> - -*** - -### createResetPasswordLink() - -```ts -createResetPasswordLink: (tenantId, userId, email, userContext?) => Promise< - | { - link: string; - status: "OK"; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}> = Wrapper.createResetPasswordLink; -``` - -Defined in: [recipe/emailpassword/index.ts:375](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L375) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -*** - -### createResetPasswordToken() - -```ts -createResetPasswordToken: (tenantId, userId, email, userContext?) => Promise< - | { - status: "OK"; - token: string; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}> = Wrapper.createResetPasswordToken; -``` - -Defined in: [recipe/emailpassword/index.ts:365](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L365) - -We do not make email optional here cause we want to -allow passing in primaryUserId. If we make email optional, -and if the user provides a primaryUserId, then it may result in two problems: - - there is no recipeUserId = input primaryUserId, in this case, - this function will throw an error - - There is a recipe userId = input primaryUserId, but that recipe has no email, - or has wrong email compared to what the user wanted to generate a reset token for. - -And we want to allow primaryUserId being passed in. - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -*** - -### Error - -```ts -Error: typeof default = Wrapper.Error; -``` - -Defined in: [recipe/emailpassword/index.ts:357](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L357) - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/emailpassword/index.ts:355](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L355) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-emailpassword-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### resetPasswordUsingToken() - -```ts -resetPasswordUsingToken: (tenantId, token, newPassword, userContext?) => Promise< - | { - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR"; -} - | { - failureReason: string; - status: "PASSWORD_POLICY_VIOLATED_ERROR"; -}> = Wrapper.resetPasswordUsingToken; -``` - -Defined in: [recipe/emailpassword/index.ts:367](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L367) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `token` | `string` | -| `newPassword` | `string` | -| `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() - -```ts -sendEmail: (input) => Promise = Wrapper.sendEmail; -``` - -Defined in: [recipe/emailpassword/index.ts:379](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L379) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeEmailPasswordPasswordResetEmailDeliveryInput`](recipe-emailpassword-types.mdx#typeemailpasswordpasswordresetemaildeliveryinput) & `object` | - -#### Returns - -`Promise`\<`void`\> - -*** - -### sendResetPasswordEmail() - -```ts -sendResetPasswordEmail: (tenantId, userId, email, userContext?) => Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR"; -}> = Wrapper.sendResetPasswordEmail; -``` - -Defined in: [recipe/emailpassword/index.ts:377](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L377) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `email` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -*** - -### signIn() - -```ts -signIn: { - (tenantId, email, password, session?, userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "WRONG_CREDENTIALS_ERROR"; -}>; - (tenantId, email, password, session, 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"; -}>; -} = Wrapper.signIn; -``` - -Defined in: [recipe/emailpassword/index.ts:361](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L361) - -#### Call Signature - -```ts -( - tenantId, - email, - password, - session?, - userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "WRONG_CREDENTIALS_ERROR"; -}>; -``` - -##### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"WRONG_CREDENTIALS_ERROR"`; -\}\> - -#### Call Signature - -```ts -( - tenantId, - email, - password, - session, - 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 - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -signUp: { - (tenantId, email, password, session?, userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; -}>; - (tenantId, email, password, session, 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"; -}>; -} = Wrapper.signUp; -``` - -Defined in: [recipe/emailpassword/index.ts:359](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L359) - -#### Call Signature - -```ts -( - tenantId, - email, - password, - session?, - userContext?): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; -}>; -``` - -##### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `status`: `"EMAIL_ALREADY_EXISTS_ERROR"`; -\}\> - -#### Call Signature - -```ts -( - tenantId, - email, - password, - session, - 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 - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -updateEmailOrPassword: (input) => 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"; -}> = Wrapper.updateEmailOrPassword; -``` - -Defined in: [recipe/emailpassword/index.ts:371](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L371) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `applyPasswordPolicy?`: `boolean`; `email?`: `string`; `password?`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `tenantIdForPasswordPolicy?`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.applyPasswordPolicy?` | `boolean` | -| `input.email?` | `string` | -| `input.password?` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.tenantIdForPasswordPolicy?` | `string` | -| `input.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() - -```ts -verifyCredentials: (tenantId, email, password, userContext?) => Promise<{ - status: "OK" | "WRONG_CREDENTIALS_ERROR"; -}> = Wrapper.verifyCredentials; -``` - -Defined in: [recipe/emailpassword/index.ts:363](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailpassword/index.ts#L363) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `email` | `string` | -| `password` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"WRONG_CREDENTIALS_ERROR"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-emailpassword-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-emailpassword-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-emailpassword-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification-types.mdx deleted file mode 100644 index 19ad8f9576..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification-types.mdx +++ /dev/null @@ -1,327 +0,0 @@ ---- -title: supertokens-node/recipe/emailverification/types -page_title: EmailVerification Types -sidebar_position: 6 -description: References documentation for the supertokens-node/recipe/emailverification/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# EmailVerification Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/emailverification/types.ts:127](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L127) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `generateEmailVerifyTokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| \{ `newSession?`: `SessionContainerInterface`; `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailverification/types.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L153) | -| `isEmailVerifiedGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `isVerified`: `boolean`; `newSession?`: `SessionContainerInterface`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailverification/types.ts:142](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L142) | -| `verifyEmailPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `newSession?`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`UserEmailInfo`](#useremailinfo); \} \| \{ `status`: `"EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/emailverification/types.ts:128](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L128) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/emailverification/types.ts:116](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L116) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/emailverification/types.ts:118](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L118) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/emailverification/types.ts:119](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L119) | -| `emailDelivery` | `EmailDeliveryIngredient`\<[`TypeEmailVerificationEmailDeliveryInput`](#typeemailverificationemaildeliveryinput)\> | [recipe/emailverification/types.ts:124](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L124) | -| `isInServerlessEnv` | `boolean` | [recipe/emailverification/types.ts:121](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L121) | -| `recipeId` | `string` | [recipe/emailverification/types.ts:120](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L120) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/emailverification/types.ts:117](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L117) | -| `req` | `BaseRequest` | [recipe/emailverification/types.ts:122](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L122) | -| `res` | `BaseResponse` | [recipe/emailverification/types.ts:123](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L123) | - -*** - -### GetEmailForRecipeUserIdFunc() - -```ts -type GetEmailForRecipeUserIdFunc = (user, recipeUserId, userContext) => Promise< - | { - email: string; - status: "OK"; -} - | { - status: "EMAIL_DOES_NOT_EXIST_ERROR" | "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/types.ts:180](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L180) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](types.mdx#user) \| `undefined` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"EMAIL_DOES_NOT_EXIST_ERROR"` \| `"UNKNOWN_USER_ID_ERROR"`; -\}\> - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/emailverification/types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L79) - -#### Methods - -##### createEmailVerificationToken() - -```ts -createEmailVerificationToken(input): Promise< - | { - status: "OK"; - token: string; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/types.ts:80](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L80) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -##### isEmailVerified() - -```ts -isEmailVerified(input): Promise; -``` - -Defined in: [recipe/emailverification/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L100) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<`boolean`\> - -##### revokeEmailVerificationTokens() - -```ts -revokeEmailVerificationTokens(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/emailverification/types.ts:102](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L102) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### unverifyEmail() - -```ts -unverifyEmail(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/emailverification/types.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L109) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### verifyEmailUsingToken() - -```ts -verifyEmailUsingToken(input): Promise< - | { - status: "OK"; - user: UserEmailInfo; -} - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/types.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L93) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `attemptAccountLinking`: `boolean`; `tenantId`: `string`; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.attemptAccountLinking` | `boolean` | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `user`: [`UserEmailInfo`](#useremailinfo); -\} - \| \{ - `status`: `"EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`; -\}\> - -*** - -### TypeEmailVerificationEmailDeliveryInput - -```ts -type TypeEmailVerificationEmailDeliveryInput = object; -``` - -Defined in: [recipe/emailverification/types.ts:166](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L166) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailVerifyLink` | `string` | [recipe/emailverification/types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L176) | -| `tenantId` | `string` | [recipe/emailverification/types.ts:177](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L177) | -| `type` | `"EMAIL_VERIFICATION"` | [recipe/emailverification/types.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L167) | -| `user` | `object` | [recipe/emailverification/types.ts:168](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L168) | -| `user.email` | `string` | [recipe/emailverification/types.ts:174](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L174) | -| `user.id` | `string` | [recipe/emailverification/types.ts:172](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L172) | -| `user.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | [recipe/emailverification/types.ts:173](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L173) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/emailverification/types.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L28) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailDelivery?` | `EmailDeliveryTypeInput`\<[`TypeEmailVerificationEmailDeliveryInput`](#typeemailverificationemaildeliveryinput)\> | [recipe/emailverification/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L30) | -| `getEmailForRecipeUserId?` | (`recipeUserId`, `userContext`) => `Promise`\< \| \{ `email`: `string`; `status`: `"OK"`; \} \| \{ `status`: `"EMAIL_DOES_NOT_EXIST_ERROR"` \| `"UNKNOWN_USER_ID_ERROR"`; \}\> | [recipe/emailverification/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L31) | -| `mode` | `"REQUIRED"` \| `"OPTIONAL"` | [recipe/emailverification/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L29) | -| `override?` | `object` | [recipe/emailverification/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L41) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/emailverification/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L46) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/emailverification/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L42) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/emailverification/types.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L50) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getEmailDeliveryConfig` | (`isInServerlessEnv`) => `EmailDeliveryTypeInputWithService`\<[`TypeEmailVerificationEmailDeliveryInput`](#typeemailverificationemaildeliveryinput)\> | [recipe/emailverification/types.ts:52](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L52) | -| `getEmailForRecipeUserId?` | (`recipeUserId`, `userContext`) => `Promise`\< \| \{ `email`: `string`; `status`: `"OK"`; \} \| \{ `status`: `"EMAIL_DOES_NOT_EXIST_ERROR"` \| `"UNKNOWN_USER_ID_ERROR"`; \}\> | [recipe/emailverification/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L55) | -| `mode` | `"REQUIRED"` \| `"OPTIONAL"` | [recipe/emailverification/types.ts:51](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L51) | -| `override` | `object` | [recipe/emailverification/types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L65) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/emailverification/types.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L70) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/emailverification/types.ts:66](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L66) | - -*** - -### UserEmailInfo - -```ts -type UserEmailInfo = object; -``` - -Defined in: [recipe/emailverification/types.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L74) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `email` | `string` | [recipe/emailverification/types.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L76) | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | [recipe/emailverification/types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/types.ts#L75) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification.mdx deleted file mode 100644 index a1e16f64ad..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-emailverification.mdx +++ /dev/null @@ -1,600 +0,0 @@ ---- -title: supertokens-node/recipe/emailverification -page_title: EmailVerification -sidebar_position: 6 -description: References documentation for the supertokens-node/recipe/emailverification package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# EmailVerification - - -## Classes - -### default - -Defined in: [recipe/emailverification/index.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L31) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `EmailVerificationClaim` | `static` | `EmailVerificationClaimClass` | `EmailVerificationClaim` | [recipe/emailverification/index.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L36) | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [recipe/emailverification/index.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L34) | -| `init` | `static` | (`config`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/emailverification/index.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L32) | - -#### Methods - -##### createEmailVerificationLink() - -```ts -static createEmailVerificationLink( - tenantId, - recipeUserId, - email?, - userContext?): Promise< - | { - link: string; - status: "OK"; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L74) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -##### createEmailVerificationToken() - -```ts -static createEmailVerificationToken( - tenantId, - recipeUserId, - email?, - userContext?): Promise< - | { - status: "OK"; - token: string; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L38) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -##### isEmailVerified() - -```ts -static isEmailVerified( - recipeUserId, - email?, -userContext?): Promise; -``` - -Defined in: [recipe/emailverification/index.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L176) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`boolean`\> - -##### revokeEmailVerificationTokens() - -```ts -static revokeEmailVerificationTokens( - tenantId, - recipeUserId, - email?, - userContext?): Promise<{ - status: string; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L198) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `string`; -\}\> - -##### sendEmail() - -```ts -static sendEmail(input): Promise; -``` - -Defined in: [recipe/emailverification/index.ts:256](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L256) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeEmailVerificationEmailDeliveryInput`](recipe-emailverification-types.mdx#typeemailverificationemaildeliveryinput) & `object` | - -###### Returns - -`Promise`\<`void`\> - -##### sendEmailVerificationEmail() - -```ts -static sendEmailVerificationEmail( - tenantId, - userId, - recipeUserId, - email?, - userContext?): Promise< - | { - status: "OK"; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L109) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -##### unverifyEmail() - -```ts -static unverifyEmail( - recipeUserId, - email?, - userContext?): Promise<{ - status: string; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:233](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L233) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `string`; -\}\> - -##### verifyEmailUsingToken() - -```ts -static verifyEmailUsingToken( - tenantId, - token, - attemptAccountLinking, - userContext?): Promise< - | { - status: "OK"; - user: UserEmailInfo; -} - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; -}>; -``` - -Defined in: [recipe/emailverification/index.ts:162](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L162) - -###### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `tenantId` | `string` | `undefined` | -| `token` | `string` | `undefined` | -| `attemptAccountLinking` | `boolean` | `true` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `user`: [`UserEmailInfo`](recipe-emailverification-types.mdx#useremailinfo); -\} - \| \{ - `status`: `"EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`; -\}\> - -## Variables - -### createEmailVerificationLink() - -```ts -createEmailVerificationLink: (tenantId, recipeUserId, email?, userContext?) => Promise< - | { - link: string; - status: "OK"; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}> = Wrapper.createEmailVerificationLink; -``` - -Defined in: [recipe/emailverification/index.ts:271](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L271) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -*** - -### createEmailVerificationToken() - -```ts -createEmailVerificationToken: (tenantId, recipeUserId, email?, userContext?) => Promise< - | { - status: "OK"; - token: string; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}> = Wrapper.createEmailVerificationToken; -``` - -Defined in: [recipe/emailverification/index.ts:269](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L269) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -*** - -### EmailVerificationClaim - -```ts -const EmailVerificationClaim: EmailVerificationClaimClass; -``` - -Defined in: [recipe/emailverification/emailVerificationClaim.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/emailVerificationClaim.ts#L68) - -*** - -### Error - -```ts -Error: typeof default = Wrapper.Error; -``` - -Defined in: [recipe/emailverification/index.ts:267](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L267) - -*** - -### init() - -```ts -init: (config) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/emailverification/index.ts:265](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L265) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config` | [`TypeInput`](recipe-emailverification-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### isEmailVerified() - -```ts -isEmailVerified: (recipeUserId, email?, userContext?) => Promise = Wrapper.isEmailVerified; -``` - -Defined in: [recipe/emailverification/index.ts:277](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L277) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`boolean`\> - -*** - -### revokeEmailVerificationTokens() - -```ts -revokeEmailVerificationTokens: (tenantId, recipeUserId, email?, userContext?) => Promise<{ - status: string; -}> = Wrapper.revokeEmailVerificationTokens; -``` - -Defined in: [recipe/emailverification/index.ts:279](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L279) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `string`; -\}\> - -*** - -### sendEmail() - -```ts -sendEmail: (input) => Promise = Wrapper.sendEmail; -``` - -Defined in: [recipe/emailverification/index.ts:285](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L285) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeEmailVerificationEmailDeliveryInput`](recipe-emailverification-types.mdx#typeemailverificationemaildeliveryinput) & `object` | - -#### Returns - -`Promise`\<`void`\> - -*** - -### sendEmailVerificationEmail() - -```ts -sendEmailVerificationEmail: (tenantId, userId, recipeUserId, email?, userContext?) => Promise< - | { - status: "OK"; -} - | { - status: "EMAIL_ALREADY_VERIFIED_ERROR"; -}> = Wrapper.sendEmailVerificationEmail; -``` - -Defined in: [recipe/emailverification/index.ts:273](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L273) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| \{ - `status`: `"EMAIL_ALREADY_VERIFIED_ERROR"`; -\}\> - -*** - -### unverifyEmail() - -```ts -unverifyEmail: (recipeUserId, email?, userContext?) => Promise<{ - status: string; -}> = Wrapper.unverifyEmail; -``` - -Defined in: [recipe/emailverification/index.ts:281](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L281) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `email?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `string`; -\}\> - -*** - -### verifyEmailUsingToken() - -```ts -verifyEmailUsingToken: (tenantId, token, attemptAccountLinking, userContext?) => Promise< - | { - status: "OK"; - user: UserEmailInfo; -} - | { - status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; -}> = Wrapper.verifyEmailUsingToken; -``` - -Defined in: [recipe/emailverification/index.ts:275](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/emailverification/index.ts#L275) - -#### Parameters - -| Parameter | Type | Default value | -| ------ | ------ | ------ | -| `tenantId` | `string` | `undefined` | -| `token` | `string` | `undefined` | -| `attemptAccountLinking` | `boolean` | `true` | -| `userContext?` | `Record`\<`string`, `any`\> | `undefined` | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `user`: [`UserEmailInfo`](recipe-emailverification-types.mdx#useremailinfo); -\} - \| \{ - `status`: `"EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-emailverification-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-emailverification-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-emailverification-types.mdx#recipeinterface) - -*** - -### UserEmailInfo - -Re-exports [UserEmailInfo](recipe-emailverification-types.mdx#useremailinfo) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt-types.mdx deleted file mode 100644 index 934c078065..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt-types.mdx +++ /dev/null @@ -1,182 +0,0 @@ ---- -title: supertokens-node/recipe/jwt/types -page_title: JWT Types -sidebar_position: 9 -description: References documentation for the supertokens-node/recipe/jwt/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# JWT Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/jwt/types.ts:82](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L82) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getJWKSGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `keys`: [`JsonWebKey`](#jsonwebkey)[]; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/jwt/types.ts:83](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L83) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/jwt/types.ts:51](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L51) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/jwt/types.ts:53](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L53) | -| `isInServerlessEnv` | `boolean` | [recipe/jwt/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L55) | -| `recipeId` | `string` | [recipe/jwt/types.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L54) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/jwt/types.ts:52](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L52) | -| `req` | `BaseRequest` | [recipe/jwt/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L56) | -| `res` | `BaseResponse` | [recipe/jwt/types.ts:57](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L57) | - -*** - -### JsonWebKey - -```ts -type JsonWebKey = object; -``` - -Defined in: [recipe/jwt/types.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L20) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `alg` | `string` | [recipe/jwt/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L25) | -| `e` | `string` | [recipe/jwt/types.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L24) | -| `kid` | `string` | [recipe/jwt/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L22) | -| `kty` | `string` | [recipe/jwt/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L21) | -| `n` | `string` | [recipe/jwt/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L23) | -| `use` | `string` | [recipe/jwt/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L26) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/jwt/types.ts:60](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L60) - -#### Methods - -##### createJWT() - -```ts -createJWT(input): Promise< - | { - jwt: string; - status: "OK"; -} - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; -}>; -``` - -Defined in: [recipe/jwt/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L61) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `payload?`: `any`; `userContext`: [`UserContext`](types.mdx#usercontext); `useStaticSigningKey?`: `boolean`; `validitySeconds?`: `number`; \} | -| `input.payload?` | `any` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.useStaticSigningKey?` | `boolean` | -| `input.validitySeconds?` | `number` | - -###### Returns - -`Promise`\< - \| \{ - `jwt`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNSUPPORTED_ALGORITHM_ERROR"`; -\}\> - -##### getJWKS() - -```ts -getJWKS(input): Promise<{ - keys: JsonWebKey[]; - validityInSeconds?: number; -}>; -``` - -Defined in: [recipe/jwt/types.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L76) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `keys`: [`JsonWebKey`](#jsonwebkey)[]; - `validityInSeconds?`: `number`; -\}\> - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/jwt/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L29) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `jwtValiditySeconds?` | `number` | [recipe/jwt/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L30) | -| `override?` | `object` | [recipe/jwt/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L31) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/jwt/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L36) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/jwt/types.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L32) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/jwt/types.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L40) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `jwtValiditySeconds` | `number` | [recipe/jwt/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L41) | -| `override` | `object` | [recipe/jwt/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L42) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/jwt/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L47) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/jwt/types.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/types.ts#L43) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt.mdx deleted file mode 100644 index 738e2e0229..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-jwt.mdx +++ /dev/null @@ -1,209 +0,0 @@ ---- -title: supertokens-node/recipe/jwt -page_title: JWT -sidebar_position: 9 -description: References documentation for the supertokens-node/recipe/jwt package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# JWT - - -## Classes - -### default - -Defined in: [recipe/jwt/index.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L20) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/jwt/index.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L21) | - -#### Methods - -##### createJWT() - -```ts -static createJWT( - payload, - validitySeconds?, - useStaticSigningKey?, - userContext?): Promise< - | { - jwt: string; - status: "OK"; -} - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; -}>; -``` - -Defined in: [recipe/jwt/index.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L23) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `payload` | `any` | -| `validitySeconds?` | `number` | -| `useStaticSigningKey?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `jwt`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNSUPPORTED_ALGORITHM_ERROR"`; -\}\> - -##### getJWKS() - -```ts -static getJWKS(userContext?): Promise<{ - keys: JsonWebKey[]; - validityInSeconds?: number; -}>; -``` - -Defined in: [recipe/jwt/index.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L37) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `keys`: [`JsonWebKey`](recipe-jwt-types.mdx#jsonwebkey)[]; - `validityInSeconds?`: `number`; -\}\> - -## Variables - -### createJWT() - -```ts -createJWT: (payload, validitySeconds?, useStaticSigningKey?, userContext?) => Promise< - | { - jwt: string; - status: "OK"; -} - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; -}> = Wrapper.createJWT; -``` - -Defined in: [recipe/jwt/index.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L45) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `payload` | `any` | -| `validitySeconds?` | `number` | -| `useStaticSigningKey?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `jwt`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNSUPPORTED_ALGORITHM_ERROR"`; -\}\> - -*** - -### getJWKS() - -```ts -getJWKS: (userContext?) => Promise<{ - keys: JsonWebKey[]; - validityInSeconds?: number; -}> = Wrapper.getJWKS; -``` - -Defined in: [recipe/jwt/index.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L46) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `keys`: [`JsonWebKey`](recipe-jwt-types.mdx#jsonwebkey)[]; - `validityInSeconds?`: `number`; -\}\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/jwt/index.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/jwt/index.ts#L44) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-jwt-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-jwt-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-jwt-types.mdx#apioptions) - -*** - -### JsonWebKey - -Re-exports [JsonWebKey](recipe-jwt-types.mdx#jsonwebkey) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-jwt-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth-types.mdx deleted file mode 100644 index 6ef673a6e5..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth-types.mdx +++ /dev/null @@ -1,277 +0,0 @@ ---- -title: supertokens-node/recipe/multifactorauth/types -page_title: MFA Types -sidebar_position: 7 -description: References documentation for the supertokens-node/recipe/multifactorauth/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# MFA Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:118](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L118) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `resyncSessionAndFetchMFAInfoPUT` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `emails`: `Record`\<`string`, `string`[] \| `undefined`\>; `factors`: \{ `allowedToSetup`: `string`[]; `alreadySetup`: `string`[]; `next`: `string`[]; \}; `phoneNumbers`: `Record`\<`string`, `string`[] \| `undefined`\>; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/multifactorauth/types.ts:119](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L119) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L108) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/multifactorauth/types.ts:111](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L111) | -| `isInServerlessEnv` | `boolean` | [recipe/multifactorauth/types.ts:113](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L113) | -| `recipeId` | `string` | [recipe/multifactorauth/types.ts:112](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L112) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/multifactorauth/types.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L109) | -| `recipeInstance` | `Recipe` | [recipe/multifactorauth/types.ts:110](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L110) | -| `req` | `BaseRequest` | [recipe/multifactorauth/types.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L114) | -| `res` | `BaseResponse` | [recipe/multifactorauth/types.ts:115](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L115) | - -*** - -### GetAllAvailableSecondaryFactorIdsFromOtherRecipesFunc() - -```ts -type GetAllAvailableSecondaryFactorIdsFromOtherRecipesFunc = (tenantConfig) => string[]; -``` - -Defined in: [recipe/multifactorauth/types.ts:138](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L138) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantConfig` | [`TenantConfig`](recipe-multitenancy-types.mdx#tenantconfig) | - -#### Returns - -`string`[] - -*** - -### GetEmailsForFactorFromOtherRecipesFunc() - -```ts -type GetEmailsForFactorFromOtherRecipesFunc = (user, sessionRecipeUserId) => - | { - factorIdToEmailsMap: Record; - status: "OK"; -} - | { - status: "UNKNOWN_SESSION_RECIPE_USER_ID"; -}; -``` - -Defined in: [recipe/multifactorauth/types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L140) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](index.mdx#user-1) | -| `sessionRecipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | - -#### Returns - - \| \{ - `factorIdToEmailsMap`: `Record`\<`string`, `string`[]\>; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_SESSION_RECIPE_USER_ID"`; -\} - -*** - -### GetFactorsSetupForUserFromOtherRecipesFunc() - -```ts -type GetFactorsSetupForUserFromOtherRecipesFunc = (user, userContext) => Promise; -``` - -Defined in: [recipe/multifactorauth/types.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L136) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](index.mdx#user-1) | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string`[]\> - -*** - -### GetPhoneNumbersForFactorsFromOtherRecipesFunc() - -```ts -type GetPhoneNumbersForFactorsFromOtherRecipesFunc = (user, sessionRecipeUserId) => - | { - factorIdToPhoneNumberMap: Record; - status: "OK"; -} - | { - status: "UNKNOWN_SESSION_RECIPE_USER_ID"; -}; -``` - -Defined in: [recipe/multifactorauth/types.ts:144](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L144) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](index.mdx#user-1) | -| `sessionRecipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | - -#### Returns - - \| \{ - `factorIdToPhoneNumberMap`: `Record`\<`string`, `string`[]\>; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_SESSION_RECIPE_USER_ID"`; -\} - -*** - -### MFAClaimValue - -```ts -type MFAClaimValue = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L36) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `c` | `Record`\<`string`, `number` \| `undefined`\> | [recipe/multifactorauth/types.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L37) | -| `v` | `boolean` | [recipe/multifactorauth/types.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L38) | - -*** - -### MFARequirementList - -```ts -type MFARequirementList = ( - | { - oneOf: string[]; -} - | { - allOfInAnyOrder: string[]; -} - | string)[]; -``` - -Defined in: [recipe/multifactorauth/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L26) - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L65) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `addToRequiredSecondaryFactorsForUser` | (`input`) => `Promise`\<`void`\> | [recipe/multifactorauth/types.ts:95](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L95) | -| `assertAllowedToSetupFactorElseThrowInvalidClaimError` | (`input`) => `Promise`\<`void`\> | [recipe/multifactorauth/types.ts:66](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L66) | -| `getFactorsSetupForUser` | (`input`) => `Promise`\<`string`[]\> | [recipe/multifactorauth/types.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L91) | -| `getMFARequirementsForAuth` | (`input`) => \| `Promise`\<[`MFARequirementList`](#mfarequirementlist)\> \| [`MFARequirementList`](#mfarequirementlist) | [recipe/multifactorauth/types.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L74) | -| `getRequiredSecondaryFactorsForUser` | (`input`) => `Promise`\<`string`[]\> | [recipe/multifactorauth/types.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L93) | -| `markFactorAsCompleteInSession` | (`input`) => `Promise`\<`void`\> | [recipe/multifactorauth/types.ts:85](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L85) | -| `removeFromRequiredSecondaryFactorsForUser` | (`input`) => `Promise`\<`void`\> | [recipe/multifactorauth/types.ts:101](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L101) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L41) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `firstFactors?` | `string`[] | [recipe/multifactorauth/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L42) | -| `override?` | `object` | [recipe/multifactorauth/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L44) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/multifactorauth/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L49) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/multifactorauth/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L45) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/multifactorauth/types.ts:53](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L53) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `firstFactors?` | `string`[] | [recipe/multifactorauth/types.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L54) | -| `override` | `object` | [recipe/multifactorauth/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L56) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/multifactorauth/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L61) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/multifactorauth/types.ts:57](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L57) | - -## Variables - -### FactorIds - -```ts -const FactorIds: object; -``` - -Defined in: [recipe/multifactorauth/types.ts:151](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L151) - -#### Type Declaration - -| Name | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | -| `EMAILPASSWORD` | `string` | `"emailpassword"` | [recipe/multifactorauth/types.ts:152](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L152) | -| `LINK_EMAIL` | `string` | `"link-email"` | [recipe/multifactorauth/types.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L156) | -| `LINK_PHONE` | `string` | `"link-phone"` | [recipe/multifactorauth/types.ts:157](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L157) | -| `OTP_EMAIL` | `string` | `"otp-email"` | [recipe/multifactorauth/types.ts:154](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L154) | -| `OTP_PHONE` | `string` | `"otp-phone"` | [recipe/multifactorauth/types.ts:155](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L155) | -| `THIRDPARTY` | `string` | `"thirdparty"` | [recipe/multifactorauth/types.ts:158](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L158) | -| `TOTP` | `string` | `"totp"` | [recipe/multifactorauth/types.ts:159](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L159) | -| `WEBAUTHN` | `string` | `"webauthn"` | [recipe/multifactorauth/types.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L153) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth.mdx deleted file mode 100644 index 53c41e3d03..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-multifactorauth.mdx +++ /dev/null @@ -1,403 +0,0 @@ ---- -title: supertokens-node/recipe/multifactorauth -page_title: MFA -sidebar_position: 7 -description: References documentation for the supertokens-node/recipe/multifactorauth package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# MFA - - -## Classes - -### default - -Defined in: [recipe/multifactorauth/index.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L25) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `FactorIds` | `static` | `object` | `FactorIds` | [recipe/multifactorauth/index.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L29) | -| `FactorIds.EMAILPASSWORD` | `public` | `string` | `"emailpassword"` | [recipe/multifactorauth/types.ts:152](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L152) | -| `FactorIds.LINK_EMAIL` | `public` | `string` | `"link-email"` | [recipe/multifactorauth/types.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L156) | -| `FactorIds.LINK_PHONE` | `public` | `string` | `"link-phone"` | [recipe/multifactorauth/types.ts:157](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L157) | -| `FactorIds.OTP_EMAIL` | `public` | `string` | `"otp-email"` | [recipe/multifactorauth/types.ts:154](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L154) | -| `FactorIds.OTP_PHONE` | `public` | `string` | `"otp-phone"` | [recipe/multifactorauth/types.ts:155](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L155) | -| `FactorIds.THIRDPARTY` | `public` | `string` | `"thirdparty"` | [recipe/multifactorauth/types.ts:158](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L158) | -| `FactorIds.TOTP` | `public` | `string` | `"totp"` | [recipe/multifactorauth/types.ts:159](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L159) | -| `FactorIds.WEBAUTHN` | `public` | `string` | `"webauthn"` | [recipe/multifactorauth/types.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/types.ts#L153) | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/multifactorauth/index.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L26) | -| `MultiFactorAuthClaim` | `static` | `MultiFactorAuthClaimClass` | `MultiFactorAuthClaim` | [recipe/multifactorauth/index.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L28) | - -#### Methods - -##### addToRequiredSecondaryFactorsForUser() - -```ts -static addToRequiredSecondaryFactorsForUser( - userId, - factorId, -userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:101](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L101) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`void`\> - -##### assertAllowedToSetupFactorElseThrowInvalidClaimError() - -```ts -static assertAllowedToSetupFactorElseThrowInvalidClaimError( - session, - factorId, -userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L31) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`void`\> - -##### getFactorsSetupForUser() - -```ts -static getFactorsSetupForUser(userId, userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:81](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L81) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`string`[]\> - -##### getMFARequirementsForAuth() - -```ts -static getMFARequirementsForAuth(session, userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:58](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L58) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`MFARequirementList`](recipe-multifactorauth-types.mdx#mfarequirementlist)\> - -##### getRequiredSecondaryFactorsForUser() - -```ts -static getRequiredSecondaryFactorsForUser(userId, userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L94) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`string`[]\> - -##### markFactorAsCompleteInSession() - -```ts -static markFactorAsCompleteInSession( - session, - factorId, -userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L69) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`void`\> - -##### removeFromRequiredSecondaryFactorsForUser() - -```ts -static removeFromRequiredSecondaryFactorsForUser( - userId, - factorId, -userContext?): Promise; -``` - -Defined in: [recipe/multifactorauth/index.ts:113](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L113) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`void`\> - -## Variables - -### addToRequiredSecondaryFactorsForUser() - -```ts -const addToRequiredSecondaryFactorsForUser: (userId, factorId, userContext?) => Promise = Wrapper.addToRequiredSecondaryFactorsForUser; -``` - -Defined in: [recipe/multifactorauth/index.ts:134](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L134) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`void`\> - -*** - -### assertAllowedToSetupFactorElseThrowInvalidClaimError() - -```ts -assertAllowedToSetupFactorElseThrowInvalidClaimError: (session, factorId, userContext?) => Promise = Wrapper.assertAllowedToSetupFactorElseThrowInvalidClaimError; -``` - -Defined in: [recipe/multifactorauth/index.ts:128](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L128) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`void`\> - -*** - -### getFactorsSetupForUser() - -```ts -getFactorsSetupForUser: (userId, userContext?) => Promise = Wrapper.getFactorsSetupForUser; -``` - -Defined in: [recipe/multifactorauth/index.ts:131](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L131) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`string`[]\> - -*** - -### getMFARequirementsForAuth() - -```ts -getMFARequirementsForAuth: (session, userContext?) => Promise = Wrapper.getMFARequirementsForAuth; -``` - -Defined in: [recipe/multifactorauth/index.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L133) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`MFARequirementList`](recipe-multifactorauth-types.mdx#mfarequirementlist)\> - -*** - -### getRequiredSecondaryFactorsForUser() - -```ts -getRequiredSecondaryFactorsForUser: (userId, userContext?) => Promise = Wrapper.getRequiredSecondaryFactorsForUser; -``` - -Defined in: [recipe/multifactorauth/index.ts:132](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L132) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`string`[]\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/multifactorauth/index.ts:126](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L126) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-multifactorauth-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### markFactorAsCompleteInSession() - -```ts -markFactorAsCompleteInSession: (session, factorId, userContext?) => Promise = Wrapper.markFactorAsCompleteInSession; -``` - -Defined in: [recipe/multifactorauth/index.ts:130](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L130) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `session` | `SessionContainerInterface` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`void`\> - -*** - -### MultiFactorAuthClaim - -```ts -const MultiFactorAuthClaim: MultiFactorAuthClaimClass; -``` - -Defined in: [recipe/multifactorauth/multiFactorAuthClaim.ts:247](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts#L247) - -*** - -### removeFromRequiredSecondaryFactorsForUser() - -```ts -const removeFromRequiredSecondaryFactorsForUser: (userId, factorId, userContext?) => Promise = Wrapper.removeFromRequiredSecondaryFactorsForUser; -``` - -Defined in: [recipe/multifactorauth/index.ts:135](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multifactorauth/index.ts#L135) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `factorId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`void`\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-multifactorauth-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-multifactorauth-types.mdx#apioptions) - -*** - -### FactorIds - -Re-exports [FactorIds](recipe-multifactorauth-types.mdx#factorids) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-multifactorauth-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy-types.mdx deleted file mode 100644 index bbe58dd925..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy-types.mdx +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: supertokens-node/recipe/multitenancy/types -page_title: MultiTenancy Types -sidebar_position: 10 -description: References documentation for the supertokens-node/recipe/multitenancy/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# MultiTenancy Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/multitenancy/types.ts:145](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L145) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `loginMethodsGET` | (`input`) => `Promise`\< \| \{ `emailPassword`: \{ `enabled`: `boolean`; \}; `firstFactors`: `string`[]; `passwordless`: \{ `enabled`: `boolean`; \}; `status`: `"OK"`; `thirdParty`: \{ `enabled`: `boolean`; `providers`: `object`[]; \}; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/multitenancy/types.ts:146](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L146) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/multitenancy/types.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L133) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `allAvailableFirstFactors` | `string`[] | [recipe/multitenancy/types.ts:141](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L141) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/multitenancy/types.ts:135](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L135) | -| `isInServerlessEnv` | `boolean` | [recipe/multitenancy/types.ts:137](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L137) | -| `recipeId` | `string` | [recipe/multitenancy/types.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L136) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/multitenancy/types.ts:134](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L134) | -| `req` | `BaseRequest` | [recipe/multitenancy/types.ts:138](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L138) | -| `res` | `BaseResponse` | [recipe/multitenancy/types.ts:139](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L139) | -| `staticFirstFactors` | `string`[] \| `undefined` | [recipe/multitenancy/types.ts:142](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L142) | -| `staticThirdPartyProviders` | [`ProviderInput`](recipe-thirdparty-types.mdx#providerinput)[] | [recipe/multitenancy/types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L140) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/multitenancy/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L55) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `associateUserToTenant` | (`input`) => `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"`; \}\> | [recipe/multitenancy/types.ts:102](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L102) | -| `createOrUpdateTenant` | (`input`) => `Promise`\<\{ `createdNew`: `boolean`; `status`: `"OK"`; \}\> | [recipe/multitenancy/types.ts:59](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L59) | -| `createOrUpdateThirdPartyConfig` | (`input`) => `Promise`\<\{ `createdNew`: `boolean`; `status`: `"OK"`; \}\> | [recipe/multitenancy/types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L87) | -| `deleteTenant` | (`input`) => `Promise`\<\{ `didExist`: `boolean`; `status`: `"OK"`; \}\> | [recipe/multitenancy/types.ts:71](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L71) | -| `deleteThirdPartyConfig` | (`input`) => `Promise`\<\{ `didConfigExist`: `boolean`; `status`: `"OK"`; \}\> | [recipe/multitenancy/types.ts:96](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L96) | -| `disassociateUserFromTenant` | (`input`) => `Promise`\<\{ `status`: `"OK"`; `wasAssociated`: `boolean`; \}\> | [recipe/multitenancy/types.ts:123](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L123) | -| `getTenant` | (`input`) => `Promise`\<`object` & [`TenantConfig`](#tenantconfig) \| `undefined`\> | [recipe/multitenancy/types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L75) | -| `getTenantId` | (`input`) => `Promise`\<`string`\> | [recipe/multitenancy/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L56) | -| `listAllTenants` | (`input`) => `Promise`\<\{ `status`: `"OK"`; `tenants`: [`TenantConfig`](#tenantconfig) & `object`[]; \}\> | [recipe/multitenancy/types.ts:81](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L81) | - -*** - -### TenantConfig - -```ts -type TenantConfig = object; -``` - -Defined in: [recipe/multitenancy/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L46) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `coreConfig` | `object` | [recipe/multitenancy/types.ts:52](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L52) | -| `firstFactors?` | `string`[] | [recipe/multitenancy/types.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L50) | -| `requiredSecondaryFactors?` | `string`[] | [recipe/multitenancy/types.ts:51](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L51) | -| `thirdParty` | `object` | [recipe/multitenancy/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L47) | -| `thirdParty.providers` | [`ProviderConfig`](recipe-thirdparty-types.mdx#providerconfig)[] | [recipe/multitenancy/types.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L48) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/multitenancy/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L22) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getAllowedDomainsForTenantId?` | (`tenantId`, `userContext`) => `Promise`\<`string`[] \| `undefined`\> | [recipe/multitenancy/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L23) | -| `override?` | `object` | [recipe/multitenancy/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L25) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/multitenancy/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L30) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/multitenancy/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L26) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/multitenancy/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L34) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getAllowedDomainsForTenantId?` | (`tenantId`, `userContext`) => `Promise`\<`string`[] \| `undefined`\> | [recipe/multitenancy/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L35) | -| `override` | `object` | [recipe/multitenancy/types.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L37) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/multitenancy/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L42) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/multitenancy/types.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/types.ts#L38) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy.mdx deleted file mode 100644 index 248cb049f8..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-multitenancy.mdx +++ /dev/null @@ -1,567 +0,0 @@ ---- -title: supertokens-node/recipe/multitenancy -page_title: MultiTenancy -sidebar_position: 10 -description: References documentation for the supertokens-node/recipe/multitenancy package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# MultiTenancy - - -## Classes - -### default - -Defined in: [recipe/multitenancy/index.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L23) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/multitenancy/index.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L24) | - -#### Methods - -##### associateUserToTenant() - -```ts -static associateUserToTenant( - tenantId, - recipeUserId, - userContext?): 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"; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:124](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L124) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `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() - -```ts -static createOrUpdateTenant( - tenantId, - config?, - userContext?): Promise<{ - createdNew: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L26) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `config?` | \{ `coreConfig?`: \{ \[`key`: `string`\]: `any`; \}; `firstFactors?`: `string`[]; `requiredSecondaryFactors?`: `string`[]; \} | -| `config.coreConfig?` | \{ \[`key`: `string`\]: `any`; \} | -| `config.firstFactors?` | `string`[] | -| `config.requiredSecondaryFactors?` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `createdNew`: `boolean`; - `status`: `"OK"`; -\}\> - -##### createOrUpdateThirdPartyConfig() - -```ts -static createOrUpdateThirdPartyConfig( - tenantId, - config, - skipValidation?, - userContext?): Promise<{ - createdNew: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:90](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L90) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `config` | [`ProviderConfig`](recipe-thirdparty-types.mdx#providerconfig) | -| `skipValidation?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `createdNew`: `boolean`; - `status`: `"OK"`; -\}\> - -##### deleteTenant() - -```ts -static deleteTenant(tenantId, userContext?): Promise<{ - didExist: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L46) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `didExist`: `boolean`; - `status`: `"OK"`; -\}\> - -##### deleteThirdPartyConfig() - -```ts -static deleteThirdPartyConfig( - tenantId, - thirdPartyId, - userContext?): Promise<{ - didConfigExist: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L108) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `didConfigExist`: `boolean`; - `status`: `"OK"`; -\}\> - -##### disassociateUserFromTenant() - -```ts -static disassociateUserFromTenant( - tenantId, - recipeUserId, - userContext?): Promise<{ - status: "OK"; - wasAssociated: boolean; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L153) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `wasAssociated`: `boolean`; -\}\> - -##### getTenant() - -```ts -static getTenant(tenantId, userContext?): Promise; -``` - -Defined in: [recipe/multitenancy/index.ts:60](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L60) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<`object` & [`TenantConfig`](recipe-multitenancy-types.mdx#tenantconfig)\> - -##### listAllTenants() - -```ts -static listAllTenants(userContext?): Promise<{ - status: "OK"; - tenants: object & TenantConfig[]; -}>; -``` - -Defined in: [recipe/multitenancy/index.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L76) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `tenants`: `object` & [`TenantConfig`](recipe-multitenancy-types.mdx#tenantconfig)[]; -\}\> - -## Variables - -### AllowedDomainsClaim - -```ts -const AllowedDomainsClaim: AllowedDomainsClaimClass; -``` - -Defined in: [recipe/multitenancy/allowedDomainsClaim.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts#L23) - -*** - -### associateUserToTenant() - -```ts -associateUserToTenant: (tenantId, recipeUserId, userContext?) => 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"; -}> = Wrapper.associateUserToTenant; -``` - -Defined in: [recipe/multitenancy/index.ts:180](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L180) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `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() - -```ts -createOrUpdateTenant: (tenantId, config?, userContext?) => Promise<{ - createdNew: boolean; - status: "OK"; -}> = Wrapper.createOrUpdateTenant; -``` - -Defined in: [recipe/multitenancy/index.ts:172](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L172) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `config?` | \{ `coreConfig?`: \{ \[`key`: `string`\]: `any`; \}; `firstFactors?`: `string`[]; `requiredSecondaryFactors?`: `string`[]; \} | -| `config.coreConfig?` | \{ \[`key`: `string`\]: `any`; \} | -| `config.firstFactors?` | `string`[] | -| `config.requiredSecondaryFactors?` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `createdNew`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### createOrUpdateThirdPartyConfig() - -```ts -createOrUpdateThirdPartyConfig: (tenantId, config, skipValidation?, userContext?) => Promise<{ - createdNew: boolean; - status: "OK"; -}> = Wrapper.createOrUpdateThirdPartyConfig; -``` - -Defined in: [recipe/multitenancy/index.ts:177](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L177) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `config` | [`ProviderConfig`](recipe-thirdparty-types.mdx#providerconfig) | -| `skipValidation?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `createdNew`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### deleteTenant() - -```ts -deleteTenant: (tenantId, userContext?) => Promise<{ - didExist: boolean; - status: "OK"; -}> = Wrapper.deleteTenant; -``` - -Defined in: [recipe/multitenancy/index.ts:173](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L173) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `didExist`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### deleteThirdPartyConfig() - -```ts -deleteThirdPartyConfig: (tenantId, thirdPartyId, userContext?) => Promise<{ - didConfigExist: boolean; - status: "OK"; -}> = Wrapper.deleteThirdPartyConfig; -``` - -Defined in: [recipe/multitenancy/index.ts:178](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L178) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `didConfigExist`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### disassociateUserFromTenant() - -```ts -disassociateUserFromTenant: (tenantId, recipeUserId, userContext?) => Promise<{ - status: "OK"; - wasAssociated: boolean; -}> = Wrapper.disassociateUserFromTenant; -``` - -Defined in: [recipe/multitenancy/index.ts:181](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L181) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `wasAssociated`: `boolean`; -\}\> - -*** - -### getTenant() - -```ts -getTenant: (tenantId, userContext?) => Promise = Wrapper.getTenant; -``` - -Defined in: [recipe/multitenancy/index.ts:174](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L174) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<`object` & [`TenantConfig`](recipe-multitenancy-types.mdx#tenantconfig)\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/multitenancy/index.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L170) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-multitenancy-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### listAllTenants() - -```ts -listAllTenants: (userContext?) => Promise<{ - status: "OK"; - tenants: object & TenantConfig[]; -}> = Wrapper.listAllTenants; -``` - -Defined in: [recipe/multitenancy/index.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/multitenancy/index.ts#L175) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; - `tenants`: `object` & [`TenantConfig`](recipe-multitenancy-types.mdx#tenantconfig)[]; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-multitenancy-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-multitenancy-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-multitenancy-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider-types.mdx deleted file mode 100644 index 81cfd3d7d6..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider-types.mdx +++ /dev/null @@ -1,1151 +0,0 @@ ---- -title: supertokens-node/recipe/oauth2provider/types -page_title: OAuth Provider Types -sidebar_position: 12 -description: References documentation for the supertokens-node/recipe/oauth2provider/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# OAuth Provider Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:419](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L419) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `authGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `cookies?`: `string`[]; `redirectTo`: `string`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:430](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L430) | -| `endSessionGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `redirectTo`: `string`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:482](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L482) | -| `endSessionPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `redirectTo`: `string`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:491](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L491) | -| `introspectTokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| [`InstrospectTokenResponse`](#instrospecttokenresponse) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| [`ErrorOAuth2`](#erroroauth2)\> | [recipe/oauth2provider/types.ts:474](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L474) | -| `loginGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `cookies?`: `string`[]; `frontendRedirectTo`: `string`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:420](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L420) | -| `loginInfoGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `info`: [`LoginInfo`](#logininfo); `status`: `"OK"`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:448](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L448) | -| `logoutPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `frontendRedirectTo`: `string`; `status`: `"OK"`; \} \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:500](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L500) | -| `revokeTokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| [`ErrorOAuth2`](#erroroauth2)\> | [recipe/oauth2provider/types.ts:465](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L465) | -| `tokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| [`TokenInfo`](#tokeninfo) \| [`ErrorOAuth2`](#erroroauth2) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:440](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L440) | -| `userInfoGET` | \| `undefined` \| (`input`) => `Promise`\< \| [`JSONObject`](types.mdx#jsonobject) \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/oauth2provider/types.ts:455](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L455) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L44) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/oauth2provider/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L46) | -| `isInServerlessEnv` | `boolean` | [recipe/oauth2provider/types.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L48) | -| `recipeId` | `string` | [recipe/oauth2provider/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L47) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/oauth2provider/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L45) | -| `req` | `BaseRequest` | [recipe/oauth2provider/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L49) | -| `res` | `BaseResponse` | [recipe/oauth2provider/types.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L50) | - -*** - -### ConsentRequest - -```ts -type ConsentRequest = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L68) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `acr?` | `string` | [recipe/oauth2provider/types.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L70) | -| `amr?` | `string`[] | [recipe/oauth2provider/types.ts:73](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L73) | -| `challenge` | `string` | [recipe/oauth2provider/types.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L76) | -| `client?` | `OAuth2Client` | [recipe/oauth2provider/types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L79) | -| `context?` | [`JSONObject`](types.mdx#jsonobject) | [recipe/oauth2provider/types.ts:82](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L82) | -| `loginChallenge?` | `string` | [recipe/oauth2provider/types.ts:85](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L85) | -| `loginSessionId?` | `string` | [recipe/oauth2provider/types.ts:88](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L88) | -| `oidcContext?` | `any` | [recipe/oauth2provider/types.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L91) | -| `requestedAccessTokenAudience?` | `string`[] | [recipe/oauth2provider/types.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L94) | -| `requestedScope?` | `string`[] | [recipe/oauth2provider/types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L97) | -| `skip?` | `boolean` | [recipe/oauth2provider/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L100) | -| `subject?` | `string` | [recipe/oauth2provider/types.ts:103](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L103) | - -*** - -### CreateOAuth2ClientInput - -```ts -type CreateOAuth2ClientInput = Partial>; -``` - -Defined in: [recipe/oauth2provider/types.ts:563](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L563) - -*** - -### DeleteOAuth2ClientInput - -```ts -type DeleteOAuth2ClientInput = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:575](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L575) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `clientId` | `string` | [recipe/oauth2provider/types.ts:576](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L576) | - -*** - -### ErrorOAuth2 - -```ts -type ErrorOAuth2 = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:53](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L53) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `error` | `string` | [recipe/oauth2provider/types.ts:58](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L58) | -| `errorDescription` | `string` | [recipe/oauth2provider/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L61) | -| `status` | `"ERROR"` | [recipe/oauth2provider/types.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L54) | -| `statusCode?` | `number` | [recipe/oauth2provider/types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L65) | - -*** - -### GetOAuth2ClientsInput - -```ts -type GetOAuth2ClientsInput = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:546](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L546) - -#### Properties - -| Property | Type | Description | Defined in | -| ------ | ------ | ------ | ------ | -| `clientName?` | `string` | The name of the clients to filter by. | [recipe/oauth2provider/types.ts:560](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L560) | -| `pageSize?` | `number` | Items per Page. Defaults to 250. | [recipe/oauth2provider/types.ts:550](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L550) | -| `paginationToken?` | `string` | Next Page Token. Defaults to "1". | [recipe/oauth2provider/types.ts:555](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L555) | - -*** - -### InstrospectTokenResponse - -```ts -type InstrospectTokenResponse = - | { - active: false; -} - | object & JSONObject; -``` - -Defined in: [recipe/oauth2provider/types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L176) - -*** - -### LoginInfo - -```ts -type LoginInfo = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:151](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L151) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `clientId` | `string` | [recipe/oauth2provider/types.ts:152](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L152) | -| `clientName` | `string` | [recipe/oauth2provider/types.ts:154](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L154) | -| `clientUri?` | `string` | [recipe/oauth2provider/types.ts:162](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L162) | -| `logoUri?` | `string` | [recipe/oauth2provider/types.ts:160](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L160) | -| `metadata?` | `Record`\<`string`, `any`\> \| `null` | [recipe/oauth2provider/types.ts:164](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L164) | -| `policyUri?` | `string` | [recipe/oauth2provider/types.ts:158](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L158) | -| `tosUri?` | `string` | [recipe/oauth2provider/types.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L156) | - -*** - -### LoginRequest - -```ts -type LoginRequest = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:106](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L106) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `challenge` | `string` | [recipe/oauth2provider/types.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L108) | -| `client` | `OAuth2Client` | [recipe/oauth2provider/types.ts:111](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L111) | -| `oidcContext?` | `any` | [recipe/oauth2provider/types.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L114) | -| `requestedAccessTokenAudience?` | `string`[] | [recipe/oauth2provider/types.ts:120](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L120) | -| `requestedScope?` | `string`[] | [recipe/oauth2provider/types.ts:123](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L123) | -| `requestUrl` | `string` | [recipe/oauth2provider/types.ts:117](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L117) | -| `sessionId?` | `string` | [recipe/oauth2provider/types.ts:126](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L126) | -| `skip` | `boolean` | [recipe/oauth2provider/types.ts:130](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L130) | -| `subject` | `string` | [recipe/oauth2provider/types.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L133) | - -*** - -### OAuth2ClientOptions - -```ts -type OAuth2ClientOptions = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:510](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L510) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `audience?` | `string`[] | [recipe/oauth2provider/types.ts:534](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L534) | -| `authorizationCodeGrantAccessTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:522](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L522) | -| `authorizationCodeGrantIdTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:523](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L523) | -| `authorizationCodeGrantRefreshTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:524](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L524) | -| `clientCredentialsGrantAccessTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:525](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L525) | -| `clientId` | `string` | [recipe/oauth2provider/types.ts:511](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L511) | -| `clientName` | `string` | [recipe/oauth2provider/types.ts:516](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L516) | -| `clientSecret?` | `string` | [recipe/oauth2provider/types.ts:512](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L512) | -| `clientUri?` | `string` | [recipe/oauth2provider/types.ts:538](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L538) | -| `createdAt` | `string` | [recipe/oauth2provider/types.ts:513](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L513) | -| `enableRefreshTokenRotation?` | `boolean` | [recipe/oauth2provider/types.ts:543](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L543) | -| `grantTypes?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:535](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L535) | -| `implicitGrantAccessTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:526](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L526) | -| `implicitGrantIdTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:527](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L527) | -| `logoUri?` | `string` | [recipe/oauth2provider/types.ts:539](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L539) | -| `metadata?` | `Record`\<`string`, `any`\> | [recipe/oauth2provider/types.ts:542](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L542) | -| `policyUri?` | `string` | [recipe/oauth2provider/types.ts:540](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L540) | -| `postLogoutRedirectUris?` | `string`[] | [recipe/oauth2provider/types.ts:520](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L520) | -| `redirectUris?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:519](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L519) | -| `refreshTokenGrantAccessTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:528](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L528) | -| `refreshTokenGrantIdTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:529](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L529) | -| `refreshTokenGrantRefreshTokenLifespan?` | `string` \| `null` | [recipe/oauth2provider/types.ts:530](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L530) | -| `responseTypes?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:536](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L536) | -| `scope` | `string` | [recipe/oauth2provider/types.ts:518](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L518) | -| `tokenEndpointAuthMethod` | `string` | [recipe/oauth2provider/types.ts:532](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L532) | -| `tosUri?` | `string` | [recipe/oauth2provider/types.ts:541](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L541) | -| `updatedAt` | `string` | [recipe/oauth2provider/types.ts:514](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L514) | - -*** - -### PayloadBuilderFunction() - -```ts -type PayloadBuilderFunction = (user, scopes, sessionHandle, userContext) => Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:579](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L579) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](index.mdx#user-1) | -| `scopes` | `string`[] | -| `sessionHandle` | `string` | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<[`JSONObject`](types.mdx#jsonobject)\> - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:178](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L178) - -#### Methods - -##### acceptConsentRequest() - -```ts -acceptConsentRequest(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:191](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L191) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `context?`: `any`; `grantAccessTokenAudience?`: `string`[]; `grantScope?`: `string`[]; `handledAt?`: `string`; `initialAccessTokenPayload`: [`JSONObject`](types.mdx#jsonobject) \| `undefined`; `initialIdTokenPayload`: [`JSONObject`](types.mdx#jsonobject) \| `undefined`; `rsub`: `string`; `sessionHandle`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.context?` | `any` | -| `input.grantAccessTokenAudience?` | `string`[] | -| `input.grantScope?` | `string`[] | -| `input.handledAt?` | `string` | -| `input.initialAccessTokenPayload` | [`JSONObject`](types.mdx#jsonobject) \| `undefined` | -| `input.initialIdTokenPayload` | [`JSONObject`](types.mdx#jsonobject) \| `undefined` | -| `input.rsub` | `string` | -| `input.sessionHandle` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; - `status`: `"OK"`; -\}\> - -##### acceptLoginRequest() - -```ts -acceptLoginRequest(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:222](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L222) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `acr?`: `string`; `amr?`: `string`[]; `challenge`: `string`; `context?`: `any`; `extendSessionLifespan?`: `boolean`; `identityProviderSessionId?`: `string`; `subject`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.acr?` | `string` | -| `input.amr?` | `string`[] | -| `input.challenge` | `string` | -| `input.context?` | `any` | -| `input.extendSessionLifespan?` | `boolean` | -| `input.identityProviderSessionId?` | `string` | -| `input.subject` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; - `status`: `"OK"`; -\}\> - -##### acceptLogoutRequest() - -```ts -acceptLogoutRequest(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:412](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L412) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; -\}\> - -##### authorization() - -```ts -authorization(input): Promise< - | ErrorOAuth2 - | { - cookies: string[] | undefined; - redirectTo: string; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:179](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L179) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `cookies`: `string` \| `undefined`; `params`: `Record`\<`string`, `string`\>; `session`: `SessionContainerInterface` \| `undefined`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.cookies` | `string` \| `undefined` | -| `input.params` | `Record`\<`string`, `string`\> | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `cookies`: `string`[] \| `undefined`; - `redirectTo`: `string`; -\}\> - -##### buildAccessTokenPayload() - -```ts -buildAccessTokenPayload(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:342](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L342) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `client`: `OAuth2Client`; `scopes`: `string`[]; `sessionHandle`: `string` \| `undefined`; `user`: [`User`](index.mdx#user-1) \| `undefined`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.client` | `OAuth2Client` | -| `input.scopes` | `string`[] | -| `input.sessionHandle` | `string` \| `undefined` | -| `input.user` | [`User`](index.mdx#user-1) \| `undefined` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`JSONObject`](types.mdx#jsonobject)\> - -##### buildIdTokenPayload() - -```ts -buildIdTokenPayload(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:349](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L349) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `client`: `OAuth2Client`; `scopes`: `string`[]; `sessionHandle`: `string` \| `undefined`; `user`: [`User`](index.mdx#user-1) \| `undefined`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.client` | `OAuth2Client` | -| `input.scopes` | `string`[] | -| `input.sessionHandle` | `string` \| `undefined` | -| `input.user` | [`User`](index.mdx#user-1) \| `undefined` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`JSONObject`](types.mdx#jsonobject)\> - -##### buildUserInfo() - -```ts -buildUserInfo(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:356](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L356) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `accessTokenPayload`: [`JSONObject`](types.mdx#jsonobject); `scopes`: `string`[]; `tenantId`: `string`; `user`: [`User`](index.mdx#user-1); `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.accessTokenPayload` | [`JSONObject`](types.mdx#jsonobject) | -| `input.scopes` | `string`[] | -| `input.tenantId` | `string` | -| `input.user` | [`User`](index.mdx#user-1) | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`JSONObject`](types.mdx#jsonobject)\> - -##### createOAuth2Client() - -```ts -createOAuth2Client(input): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:279](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L279) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | `Partial`\<`Omit`\<[`OAuth2ClientOptions`](#oauth2clientoptions), `"createdAt"` \| `"updatedAt"`\>\> & `object` | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### deleteOAuth2Client() - -```ts -deleteOAuth2Client(input): Promise< - | { - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:309](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L309) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`DeleteOAuth2ClientInput`](#deleteoauth2clientinput) & `object` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### endSession() - -```ts -endSession(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:406](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L406) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `params`: `Record`\<`string`, `string`\>; `session?`: `SessionContainerInterface`; `shouldTryRefresh`: `boolean`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.params` | `Record`\<`string`, `string`\> | -| `input.session?` | `SessionContainerInterface` | -| `input.shouldTryRefresh` | `boolean` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; -\}\> - -##### getConsentRequest() - -```ts -getConsentRequest(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:190](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L190) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`ErrorOAuth2`](#erroroauth2) \| [`ConsentRequest`](#consentrequest)\> - -##### getFrontendRedirectionURL() - -```ts -getFrontendRedirectionURL(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:363](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L363) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `forceFreshAuth`: `boolean`; `hint`: `string` \| `undefined`; `loginChallenge`: `string`; `tenantId`: `string`; `type`: `"login"`; `userContext`: [`UserContext`](types.mdx#usercontext); \} \| \{ `loginChallenge`: `string`; `type`: `"try-refresh"`; `userContext`: [`UserContext`](types.mdx#usercontext); \} \| \{ `logoutChallenge`: `string`; `type`: `"logout-confirmation"`; `userContext`: [`UserContext`](types.mdx#usercontext); \} \| \{ `type`: `"post-logout-fallback"`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | - -###### Returns - -`Promise`\<`string`\> - -##### getLoginRequest() - -```ts -getLoginRequest(input): Promise< - | ErrorOAuth2 -| LoginRequest & object>; -``` - -Defined in: [recipe/oauth2provider/types.ts:218](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L218) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| [`LoginRequest`](#loginrequest) & `object`\> - -##### getOAuth2Client() - -```ts -getOAuth2Client(input): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:252](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L252) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `clientId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.clientId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### getOAuth2Clients() - -```ts -getOAuth2Clients(input): Promise< - | { - clients: OAuth2Client[]; - nextPaginationToken?: string; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:263](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L263) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`GetOAuth2ClientsInput`](#getoauth2clientsinput) & `object` | - -###### Returns - -`Promise`\< - \| \{ - `clients`: `OAuth2Client`[]; - `nextPaginationToken?`: `string`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### getRequestedScopes() - -```ts -getRequestedScopes(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:335](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L335) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `clientId`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1) \| `undefined`; `scopeParam`: `string`[]; `sessionHandle`: `string` \| `undefined`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.clientId` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) \| `undefined` | -| `input.scopeParam` | `string`[] | -| `input.sessionHandle` | `string` \| `undefined` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<`string`[]\> - -##### introspectToken() - -```ts -introspectToken(input): Promise< - | InstrospectTokenResponse & { status: "OK"; } -| ErrorOAuth2>; -``` - -Defined in: [recipe/oauth2provider/types.ts:401](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L401) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `scopes?`: `string`[]; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.scopes?` | `string`[] | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| InstrospectTokenResponse & \{ status: "OK"; \} - \| [`ErrorOAuth2`](#erroroauth2)\> - -##### rejectConsentRequest() - -```ts -rejectConsentRequest(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:212](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L212) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `error`: [`ErrorOAuth2`](#erroroauth2); `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.error` | [`ErrorOAuth2`](#erroroauth2) | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; - `status`: `"OK"`; -\}\> - -##### rejectLoginRequest() - -```ts -rejectLoginRequest(input): Promise< - | ErrorOAuth2 - | { - redirectTo: string; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:246](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L246) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `error`: [`ErrorOAuth2`](#erroroauth2); `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.error` | [`ErrorOAuth2`](#erroroauth2) | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `redirectTo`: `string`; - `status`: `"OK"`; -\}\> - -##### rejectLogoutRequest() - -```ts -rejectLogoutRequest(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:416](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L416) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `challenge`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.challenge` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### revokeToken() - -```ts -revokeToken(input): Promise< - | ErrorOAuth2 - | { - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:388](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L388) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | `object` & \| \{ `authorizationHeader`: `string`; \} \| \{ `clientId`: `string`; `clientSecret?`: `string`; \} | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](#erroroauth2) - \| \{ - `status`: `"OK"`; -\}\> - -##### revokeTokensByClientId() - -```ts -revokeTokensByClientId(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:399](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L399) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `clientId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.clientId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### revokeTokensBySessionHandle() - -```ts -revokeTokensBySessionHandle(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:400](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L400) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `sessionHandle`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.sessionHandle` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### tokenExchange() - -```ts -tokenExchange(input): Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:185](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L185) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `authorizationHeader?`: `string`; `body`: `Record`\<`string`, `string` \| `undefined`\>; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.authorizationHeader?` | `string` | -| `input.body` | `Record`\<`string`, `string` \| `undefined`\> | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`ErrorOAuth2`](#erroroauth2) \| [`TokenInfo`](#tokeninfo)\> - -##### updateOAuth2Client() - -```ts -updateOAuth2Client(input): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:294](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L294) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`NonNullableProperties`](types.mdx#nonnullableproperties)\<`Omit`\<`Partial`\<`Omit`\<[`OAuth2ClientOptions`](#oauth2clientoptions), `"createdAt"` \| `"updatedAt"`\>\>, `"redirectUris"` \| `"grantTypes"` \| `"responseTypes"` \| `"metadata"`\>\> & `object` & `object` | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### validateOAuth2AccessToken() - -```ts -validateOAuth2AccessToken(input): Promise<{ - payload: JSONObject; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/types.ts:324](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L324) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `checkDatabase?`: `boolean`; `requirements?`: \{ `audience?`: `string`; `clientId?`: `string`; `scopes?`: `string`[]; \}; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.checkDatabase?` | `boolean` | -| `input.requirements?` | \{ `audience?`: `string`; `clientId?`: `string`; `scopes?`: `string`[]; \} | -| `input.requirements.audience?` | `string` | -| `input.requirements.clientId?` | `string` | -| `input.requirements.scopes?` | `string`[] | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `payload`: [`JSONObject`](types.mdx#jsonobject); - `status`: `"OK"`; -\}\> - -*** - -### TokenInfo - -```ts -type TokenInfo = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L136) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `access_token?` | `string` | [recipe/oauth2provider/types.ts:138](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L138) | -| `expires_in` | `number` | [recipe/oauth2provider/types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L140) | -| `id_token?` | `string` | [recipe/oauth2provider/types.ts:142](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L142) | -| `refresh_token?` | `string` | [recipe/oauth2provider/types.ts:144](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L144) | -| `scope` | `string` | [recipe/oauth2provider/types.ts:146](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L146) | -| `token_type` | `string` | [recipe/oauth2provider/types.ts:148](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L148) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L24) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override?` | `object` | [recipe/oauth2provider/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L25) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/oauth2provider/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L30) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/oauth2provider/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L26) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L34) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override` | `object` | [recipe/oauth2provider/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L35) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/oauth2provider/types.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L40) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/oauth2provider/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L36) | - -*** - -### UpdateOAuth2ClientInput - -```ts -type UpdateOAuth2ClientInput = NonNullableProperties> & object; -``` - -Defined in: [recipe/oauth2provider/types.ts:565](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L565) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `clientId` | `string` | [recipe/oauth2provider/types.ts:568](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L568) | -| `grantTypes?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:570](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L570) | -| `metadata?` | `Record`\<`string`, `any`\> \| `null` | [recipe/oauth2provider/types.ts:572](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L572) | -| `redirectUris?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:569](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L569) | -| `responseTypes?` | `string`[] \| `null` | [recipe/oauth2provider/types.ts:571](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L571) | - -*** - -### UserInfo - -```ts -type UserInfo = object; -``` - -Defined in: [recipe/oauth2provider/types.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L167) - -#### Indexable - -```ts -[key: string]: JSONValue -``` - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `email?` | `string` | [recipe/oauth2provider/types.ts:169](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L169) | -| `email_verified?` | `boolean` | [recipe/oauth2provider/types.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L170) | -| `phoneNumber?` | `string` | [recipe/oauth2provider/types.ts:171](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L171) | -| `phoneNumber_verified?` | `boolean` | [recipe/oauth2provider/types.ts:172](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L172) | -| `sub` | `string` | [recipe/oauth2provider/types.ts:168](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L168) | - -*** - -### UserInfoBuilderFunction() - -```ts -type UserInfoBuilderFunction = (user, accessTokenPayload, scopes, tenantId, userContext) => Promise; -``` - -Defined in: [recipe/oauth2provider/types.ts:585](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/types.ts#L585) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `user` | [`User`](index.mdx#user-1) | -| `accessTokenPayload` | [`JSONObject`](types.mdx#jsonobject) | -| `scopes` | `string`[] | -| `tenantId` | `string` | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<[`JSONObject`](types.mdx#jsonobject)\> diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider.mdx deleted file mode 100644 index a10e4269f0..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-oauth2provider.mdx +++ /dev/null @@ -1,795 +0,0 @@ ---- -title: supertokens-node/recipe/oauth2provider -page_title: OAuth Provider -sidebar_position: 12 -description: References documentation for the supertokens-node/recipe/oauth2provider package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# OAuth Provider - - -## Classes - -### default - -Defined in: [recipe/oauth2provider/index.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L28) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/oauth2provider/index.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L29) | - -#### Methods - -##### createOAuth2Client() - -```ts -static createOAuth2Client(input, userContext?): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L43) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`CreateOAuth2ClientInput`](recipe-oauth2provider-types.mdx#createoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### createTokenForClientCredentials() - -```ts -static createTokenForClientCredentials( - clientId, - clientSecret, - scope?, - audience?, - userContext?): Promise< - | ErrorOAuth2 -| TokenInfo>; -``` - -Defined in: [recipe/oauth2provider/index.ts:80](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L80) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `clientSecret` | `string` | -| `scope?` | `string`[] | -| `audience?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2) - \| [`TokenInfo`](recipe-oauth2provider-types.mdx#tokeninfo)\> - -##### deleteOAuth2Client() - -```ts -static deleteOAuth2Client(input, userContext?): Promise< - | { - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L55) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`DeleteOAuth2ClientInput`](recipe-oauth2provider-types.mdx#deleteoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### getOAuth2Client() - -```ts -static getOAuth2Client(clientId, userContext?): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L31) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### getOAuth2Clients() - -```ts -static getOAuth2Clients(input, userContext?): Promise< - | { - clients: OAuth2Client[]; - nextPaginationToken?: string; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L37) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`GetOAuth2ClientsInput`](recipe-oauth2provider-types.mdx#getoauth2clientsinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `clients`: `OAuth2Client`[]; - `nextPaginationToken?`: `string`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### revokeToken() - -```ts -static revokeToken( - token, - clientId, - clientSecret?, - userContext?): Promise< - | ErrorOAuth2 - | { - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:99](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L99) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `clientId` | `string` | -| `clientSecret?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2) - \| \{ - `status`: `"OK"`; -\}\> - -##### revokeTokensByClientId() - -```ts -static revokeTokensByClientId(clientId, userContext?): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L140) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### revokeTokensBySessionHandle() - -```ts -static revokeTokensBySessionHandle(sessionHandle, userContext?): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:147](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L147) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `sessionHandle` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### updateOAuth2Client() - -```ts -static updateOAuth2Client(input, userContext?): Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L49) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`UpdateOAuth2ClientInput`](recipe-oauth2provider-types.mdx#updateoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -##### validateOAuth2AccessToken() - -```ts -static validateOAuth2AccessToken( - token, - requirements?, - checkDatabase?, - userContext?): Promise<{ - payload: JSONObject; - status: "OK"; -}>; -``` - -Defined in: [recipe/oauth2provider/index.ts:62](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L62) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `requirements?` | \{ `audience?`: `string`; `clientId?`: `string`; `scopes?`: `string`[]; \} | -| `requirements.audience?` | `string` | -| `requirements.clientId?` | `string` | -| `requirements.scopes?` | `string`[] | -| `checkDatabase?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `payload`: [`JSONObject`](types.mdx#jsonobject); - `status`: `"OK"`; -\}\> - -##### validateOAuth2RefreshToken() - -```ts -static validateOAuth2RefreshToken( - token, - scopes?, - userContext?): Promise< - | InstrospectTokenResponse & { status: "OK"; } -| ErrorOAuth2>; -``` - -Defined in: [recipe/oauth2provider/index.ts:154](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L154) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `scopes?` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| InstrospectTokenResponse & \{ status: "OK"; \} - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2)\> - -## Variables - -### createOAuth2Client() - -```ts -createOAuth2Client: (input, userContext?) => Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}> = Wrapper.createOAuth2Client; -``` - -Defined in: [recipe/oauth2provider/index.ts:168](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L168) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`CreateOAuth2ClientInput`](recipe-oauth2provider-types.mdx#createoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -*** - -### createTokenForClientCredentials() - -```ts -createTokenForClientCredentials: (clientId, clientSecret, scope?, audience?, userContext?) => Promise< - | ErrorOAuth2 - | TokenInfo> = Wrapper.createTokenForClientCredentials; -``` - -Defined in: [recipe/oauth2provider/index.ts:177](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L177) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `clientSecret` | `string` | -| `scope?` | `string`[] | -| `audience?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2) - \| [`TokenInfo`](recipe-oauth2provider-types.mdx#tokeninfo)\> - -*** - -### deleteOAuth2Client() - -```ts -deleteOAuth2Client: (input, userContext?) => Promise< - | { - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}> = Wrapper.deleteOAuth2Client; -``` - -Defined in: [recipe/oauth2provider/index.ts:172](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L172) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`DeleteOAuth2ClientInput`](recipe-oauth2provider-types.mdx#deleteoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -*** - -### getOAuth2Client() - -```ts -getOAuth2Client: (clientId, userContext?) => Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}> = Wrapper.getOAuth2Client; -``` - -Defined in: [recipe/oauth2provider/index.ts:165](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L165) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -*** - -### getOAuth2Clients() - -```ts -getOAuth2Clients: (input, userContext?) => Promise< - | { - clients: OAuth2Client[]; - nextPaginationToken?: string; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}> = Wrapper.getOAuth2Clients; -``` - -Defined in: [recipe/oauth2provider/index.ts:166](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L166) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`GetOAuth2ClientsInput`](recipe-oauth2provider-types.mdx#getoauth2clientsinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `clients`: `OAuth2Client`[]; - `nextPaginationToken?`: `string`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/oauth2provider/index.ts:163](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L163) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-oauth2provider-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### revokeToken() - -```ts -revokeToken: (token, clientId, clientSecret?, userContext?) => Promise< - | ErrorOAuth2 - | { - status: "OK"; -}> = Wrapper.revokeToken; -``` - -Defined in: [recipe/oauth2provider/index.ts:179](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L179) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `clientId` | `string` | -| `clientSecret?` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2) - \| \{ - `status`: `"OK"`; -\}\> - -*** - -### revokeTokensByClientId() - -```ts -revokeTokensByClientId: (clientId, userContext?) => Promise<{ - status: "OK"; -}> = Wrapper.revokeTokensByClientId; -``` - -Defined in: [recipe/oauth2provider/index.ts:180](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L180) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `clientId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### revokeTokensBySessionHandle() - -```ts -revokeTokensBySessionHandle: (sessionHandle, userContext?) => Promise<{ - status: "OK"; -}> = Wrapper.revokeTokensBySessionHandle; -``` - -Defined in: [recipe/oauth2provider/index.ts:181](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L181) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `sessionHandle` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### updateOAuth2Client() - -```ts -updateOAuth2Client: (input, userContext?) => Promise< - | { - client: OAuth2Client; - status: "OK"; -} - | { - error: string; - errorDescription: string; - status: "ERROR"; -}> = Wrapper.updateOAuth2Client; -``` - -Defined in: [recipe/oauth2provider/index.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L170) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`UpdateOAuth2ClientInput`](recipe-oauth2provider-types.mdx#updateoauth2clientinput) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `client`: `OAuth2Client`; - `status`: `"OK"`; -\} - \| \{ - `error`: `string`; - `errorDescription`: `string`; - `status`: `"ERROR"`; -\}\> - -*** - -### validateOAuth2AccessToken() - -```ts -validateOAuth2AccessToken: (token, requirements?, checkDatabase?, userContext?) => Promise<{ - payload: JSONObject; - status: "OK"; -}> = Wrapper.validateOAuth2AccessToken; -``` - -Defined in: [recipe/oauth2provider/index.ts:174](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L174) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `requirements?` | \{ `audience?`: `string`; `clientId?`: `string`; `scopes?`: `string`[]; \} | -| `requirements.audience?` | `string` | -| `requirements.clientId?` | `string` | -| `requirements.scopes?` | `string`[] | -| `checkDatabase?` | `boolean` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `payload`: [`JSONObject`](types.mdx#jsonobject); - `status`: `"OK"`; -\}\> - -*** - -### validateOAuth2RefreshToken() - -```ts -validateOAuth2RefreshToken: (token, scopes?, userContext?) => Promise< - | InstrospectTokenResponse & { status: "OK"; } - | ErrorOAuth2> = Wrapper.validateOAuth2RefreshToken; -``` - -Defined in: [recipe/oauth2provider/index.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/oauth2provider/index.ts#L175) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `token` | `string` | -| `scopes?` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| InstrospectTokenResponse & \{ status: "OK"; \} - \| [`ErrorOAuth2`](recipe-oauth2provider-types.mdx#erroroauth2)\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-oauth2provider-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-oauth2provider-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-oauth2provider-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-openid-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-openid-types.mdx deleted file mode 100644 index ab616c386e..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-openid-types.mdx +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: supertokens-node/recipe/openid/types -page_title: OpenID Types -sidebar_position: 13 -description: References documentation for the supertokens-node/recipe/openid/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# OpenID Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/openid/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L47) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getOpenIdDiscoveryConfigurationGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `authorization_endpoint`: `string`; `end_session_endpoint`: `string`; `id_token_signing_alg_values_supported`: `string`[]; `issuer`: `string`; `jwks_uri`: `string`; `response_types_supported`: `string`[]; `revocation_endpoint`: `string`; `status`: `"OK"`; `subject_types_supported`: `string`[]; `token_endpoint`: `string`; `token_introspection_endpoint`: `string`; `userinfo_endpoint`: `string`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/openid/types.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L48) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/openid/types.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L39) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/openid/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L41) | -| `recipeId` | `string` | [recipe/openid/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L42) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/openid/types.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L40) | -| `req` | `BaseRequest` | [recipe/openid/types.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L43) | -| `res` | `BaseResponse` | [recipe/openid/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L44) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/openid/types.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L69) - -#### Methods - -##### createJWT() - -```ts -createJWT(input): Promise< - | { - jwt: string; - status: "OK"; -} - | { - status: "UNSUPPORTED_ALGORITHM_ERROR"; -}>; -``` - -Defined in: [recipe/openid/types.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L84) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `payload?`: `any`; `userContext`: [`UserContext`](types.mdx#usercontext); `useStaticSigningKey?`: `boolean`; `validitySeconds?`: `number`; \} | -| `input.payload?` | `any` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.useStaticSigningKey?` | `boolean` | -| `input.validitySeconds?` | `number` | - -###### Returns - -`Promise`\< - \| \{ - `jwt`: `string`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNSUPPORTED_ALGORITHM_ERROR"`; -\}\> - -##### getOpenIdDiscoveryConfiguration() - -```ts -getOpenIdDiscoveryConfiguration(input): Promise<{ - authorization_endpoint: string; - end_session_endpoint: string; - id_token_signing_alg_values_supported: string[]; - issuer: string; - jwks_uri: string; - response_types_supported: string[]; - revocation_endpoint: string; - status: "OK"; - subject_types_supported: string[]; - token_endpoint: string; - token_introspection_endpoint: string; - userinfo_endpoint: string; -}>; -``` - -Defined in: [recipe/openid/types.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L70) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `authorization_endpoint`: `string`; - `end_session_endpoint`: `string`; - `id_token_signing_alg_values_supported`: `string`[]; - `issuer`: `string`; - `jwks_uri`: `string`; - `response_types_supported`: `string`[]; - `revocation_endpoint`: `string`; - `status`: `"OK"`; - `subject_types_supported`: `string`[]; - `token_endpoint`: `string`; - `token_introspection_endpoint`: `string`; - `userinfo_endpoint`: `string`; -\}\> - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/openid/types.ts:19](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L19) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override?` | `object` | [recipe/openid/types.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L20) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/openid/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L25) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/openid/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L21) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/openid/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L29) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override` | `object` | [recipe/openid/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L30) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/openid/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L35) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/openid/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/types.ts#L31) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-openid.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-openid.mdx deleted file mode 100644 index 751c03a2fd..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-openid.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: supertokens-node/recipe/openid -page_title: OpenID -sidebar_position: 13 -description: References documentation for the supertokens-node/recipe/openid package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# OpenID - - -## Classes - -### default - -Defined in: [recipe/openid/index.ts:4](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/index.ts#L4) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `OpenIdRecipe.init` | [recipe/openid/index.ts:5](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/index.ts#L5) | - -#### Methods - -##### getOpenIdDiscoveryConfiguration() - -```ts -static getOpenIdDiscoveryConfiguration(userContext?): Promise<{ - authorization_endpoint: string; - end_session_endpoint: string; - id_token_signing_alg_values_supported: string[]; - issuer: string; - jwks_uri: string; - response_types_supported: string[]; - revocation_endpoint: string; - status: "OK"; - subject_types_supported: string[]; - token_endpoint: string; - token_introspection_endpoint: string; - userinfo_endpoint: string; -}>; -``` - -Defined in: [recipe/openid/index.ts:7](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/index.ts#L7) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `authorization_endpoint`: `string`; - `end_session_endpoint`: `string`; - `id_token_signing_alg_values_supported`: `string`[]; - `issuer`: `string`; - `jwks_uri`: `string`; - `response_types_supported`: `string`[]; - `revocation_endpoint`: `string`; - `status`: `"OK"`; - `subject_types_supported`: `string`[]; - `token_endpoint`: `string`; - `token_introspection_endpoint`: `string`; - `userinfo_endpoint`: `string`; -\}\> - -## Variables - -### getOpenIdDiscoveryConfiguration() - -```ts -getOpenIdDiscoveryConfiguration: (userContext?) => Promise<{ - authorization_endpoint: string; - end_session_endpoint: string; - id_token_signing_alg_values_supported: string[]; - issuer: string; - jwks_uri: string; - response_types_supported: string[]; - revocation_endpoint: string; - status: "OK"; - subject_types_supported: string[]; - token_endpoint: string; - token_introspection_endpoint: string; - userinfo_endpoint: string; -}> = OpenIdRecipeWrapper.getOpenIdDiscoveryConfiguration; -``` - -Defined in: [recipe/openid/index.ts:15](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/index.ts#L15) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `authorization_endpoint`: `string`; - `end_session_endpoint`: `string`; - `id_token_signing_alg_values_supported`: `string`[]; - `issuer`: `string`; - `jwks_uri`: `string`; - `response_types_supported`: `string`[]; - `revocation_endpoint`: `string`; - `status`: `"OK"`; - `subject_types_supported`: `string`[]; - `token_endpoint`: `string`; - `token_introspection_endpoint`: `string`; - `userinfo_endpoint`: `string`; -\}\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = OpenIdRecipeWrapper.init; -``` - -Defined in: [recipe/openid/index.ts:14](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/openid/index.ts#L14) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-openid-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless-types.mdx deleted file mode 100644 index 6aa78ff26d..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless-types.mdx +++ /dev/null @@ -1,219 +0,0 @@ ---- -title: supertokens-node/recipe/passwordless/types -page_title: Passwordless Types -sidebar_position: 3 -description: References documentation for the supertokens-node/recipe/passwordless/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Passwordless Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/passwordless/types.ts:335](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L335) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `consumeCodePOST?` | (`input`) => `Promise`\< \| \{ `createdNewRecipeUser`: `boolean`; `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#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"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/passwordless/types.ts:368](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L368) | -| `createCodePOST?` | (`input`) => `Promise`\< \| \{ `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"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/passwordless/types.ts:336](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L336) | -| `emailExistsGET?` | (`input`) => `Promise`\< \| \{ `exists`: `boolean`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/passwordless/types.ts:406](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L406) | -| `phoneNumberExistsGET?` | (`input`) => `Promise`\< \| \{ `exists`: `boolean`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/passwordless/types.ts:419](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L419) | -| `resendCodePOST?` | (`input`) => `Promise`\< \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| \{ `status`: `"RESTART_FLOW_ERROR"` \| `"OK"`; \}\> | [recipe/passwordless/types.ts:358](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L358) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/passwordless/types.ts:323](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L323) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/passwordless/types.ts:325](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L325) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/passwordless/types.ts:326](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L326) | -| `emailDelivery` | `EmailDeliveryIngredient`\<[`TypePasswordlessEmailDeliveryInput`](#typepasswordlessemaildeliveryinput)\> | [recipe/passwordless/types.ts:331](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L331) | -| `isInServerlessEnv` | `boolean` | [recipe/passwordless/types.ts:328](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L328) | -| `recipeId` | `string` | [recipe/passwordless/types.ts:327](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L327) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/passwordless/types.ts:324](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L324) | -| `req` | `BaseRequest` | [recipe/passwordless/types.ts:329](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L329) | -| `res` | `BaseResponse` | [recipe/passwordless/types.ts:330](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L330) | -| `smsDelivery` | `SmsDeliveryIngredient`\<[`TypePasswordlessSmsDeliveryInput`](#typepasswordlesssmsdeliveryinput)\> | [recipe/passwordless/types.ts:332](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L332) | - -*** - -### DeviceType - -```ts -type DeviceType = object; -``` - -Defined in: [recipe/passwordless/types.ts:308](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L308) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `codes` | `object`[] | [recipe/passwordless/types.ts:316](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L316) | -| `email?` | `string` | [recipe/passwordless/types.ts:313](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L313) | -| `failedCodeInputAttemptCount` | `number` | [recipe/passwordless/types.ts:311](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L311) | -| `phoneNumber?` | `string` | [recipe/passwordless/types.ts:314](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L314) | -| `preAuthSessionId` | `string` | [recipe/passwordless/types.ts:309](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L309) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/passwordless/types.ts:110](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L110) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `checkCode` | (`input`) => `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"`; \}\> | [recipe/passwordless/types.ts:203](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L203) | -| `consumeCode` | (`input`) => `Promise`\< \| \{ `consumedDevice`: \{ `email?`: `string`; `failedCodeInputAttemptCount`: `number`; `phoneNumber?`: `string`; `preAuthSessionId`: `string`; \}; `createdNewRecipeUser`: `boolean`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `status`: `"OK"`; `user`: [`User`](types.mdx#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"`; \}\> | [recipe/passwordless/types.ts:155](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L155) | -| `createCode` | (`input`) => `Promise`\<\{ `codeId`: `string`; `codeLifetime`: `number`; `deviceId`: `string`; `linkCode`: `string`; `preAuthSessionId`: `string`; `status`: `"OK"`; `timeCreated`: `number`; `userInputCode`: `string`; \}\> | [recipe/passwordless/types.ts:111](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L111) | -| `createNewCodeForDevice` | (`input`) => `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"`; \}\> | [recipe/passwordless/types.ts:137](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L137) | -| `listCodesByDeviceId` | (`input`) => `Promise`\<[`DeviceType`](#devicetype) \| `undefined`\> | [recipe/passwordless/types.ts:295](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L295) | -| `listCodesByEmail` | (`input`) => `Promise`\<[`DeviceType`](#devicetype)[]\> | [recipe/passwordless/types.ts:287](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L287) | -| `listCodesByPhoneNumber` | (`input`) => `Promise`\<[`DeviceType`](#devicetype)[]\> | [recipe/passwordless/types.ts:289](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L289) | -| `listCodesByPreAuthSessionId` | (`input`) => `Promise`\<[`DeviceType`](#devicetype) \| `undefined`\> | [recipe/passwordless/types.ts:301](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L301) | -| `revokeAllCodes` | (`input`) => `Promise`\<\{ `status`: `"OK"`; \}\> | [recipe/passwordless/types.ts:255](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L255) | -| `revokeCode` | (`input`) => `Promise`\<\{ `status`: `"OK"`; \}\> | [recipe/passwordless/types.ts:271](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L271) | -| `updateUser` | (`input`) => `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"`; \}\> | [recipe/passwordless/types.ts:236](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L236) | - -*** - -### TypeInput - -```ts -type TypeInput = - | { - contactMethod: "PHONE"; - validatePhoneNumber?: (phoneNumber, tenantId) => Promise | string | undefined; -} - | { - contactMethod: "EMAIL"; - validateEmailAddress?: (email, tenantId) => Promise | string | undefined; -} - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress?: (email, tenantId) => Promise | string | undefined; - validatePhoneNumber?: (phoneNumber, tenantId) => Promise | string | undefined; -} & object; -``` - -Defined in: [recipe/passwordless/types.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L33) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `emailDelivery?` | `EmailDeliveryTypeInput`\<[`TypePasswordlessEmailDeliveryInput`](#typepasswordlessemaildeliveryinput)\> | [recipe/passwordless/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L56) | -| `flowType` | `"USER_INPUT_CODE"` \| `"MAGIC_LINK"` \| `"USER_INPUT_CODE_AND_MAGIC_LINK"` | [recipe/passwordless/types.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L54) | -| `getCustomUserInputCode()?` | (`tenantId`, `userContext`) => `Promise`\<`string`\> \| `string` | [recipe/passwordless/types.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L61) | -| `override?` | `object` | [recipe/passwordless/types.ts:63](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L63) | -| `override.apis()?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/passwordless/types.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L68) | -| `override.functions()?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/passwordless/types.ts:64](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L64) | -| `smsDelivery?` | `SmsDeliveryTypeInput`\<[`TypePasswordlessSmsDeliveryInput`](#typepasswordlesssmsdeliveryinput)\> | [recipe/passwordless/types.ts:57](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L57) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = - | { - contactMethod: "PHONE"; - validatePhoneNumber: (phoneNumber, tenantId) => Promise | string | undefined; -} - | { - contactMethod: "EMAIL"; - validateEmailAddress: (email, tenantId) => Promise | string | undefined; -} - | { - contactMethod: "EMAIL_OR_PHONE"; - validateEmailAddress: (email, tenantId) => Promise | string | undefined; - validatePhoneNumber: (phoneNumber, tenantId) => Promise | string | undefined; -} & object; -``` - -Defined in: [recipe/passwordless/types.ts:72](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L72) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `flowType` | `"USER_INPUT_CODE"` \| `"MAGIC_LINK"` \| `"USER_INPUT_CODE_AND_MAGIC_LINK"` | [recipe/passwordless/types.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L93) | -| `getCustomUserInputCode()?` | (`tenantId`, `userContext`) => `Promise`\<`string`\> \| `string` | [recipe/passwordless/types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L97) | -| `getEmailDeliveryConfig()` | () => `EmailDeliveryTypeInputWithService`\<[`TypePasswordlessEmailDeliveryInput`](#typepasswordlessemaildeliveryinput)\> | [recipe/passwordless/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L100) | -| `getSmsDeliveryConfig()` | () => `SmsDeliveryTypeInputWithService`\<[`TypePasswordlessSmsDeliveryInput`](#typepasswordlesssmsdeliveryinput)\> | [recipe/passwordless/types.ts:99](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L99) | -| `override` | `object` | [recipe/passwordless/types.ts:101](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L101) | -| `override.apis()` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/passwordless/types.ts:106](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L106) | -| `override.functions()` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/passwordless/types.ts:102](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L102) | - -*** - -### TypePasswordlessEmailDeliveryInput - -```ts -type TypePasswordlessEmailDeliveryInput = object; -``` - -Defined in: [recipe/passwordless/types.ts:433](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L433) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `codeLifetime` | `number` | [recipe/passwordless/types.ts:439](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L439) | -| `email` | `string` | [recipe/passwordless/types.ts:436](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L436) | -| `isFirstFactor` | `boolean` | [recipe/passwordless/types.ts:435](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L435) | -| `preAuthSessionId` | `string` | [recipe/passwordless/types.ts:440](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L440) | -| `tenantId` | `string` | [recipe/passwordless/types.ts:441](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L441) | -| `type` | `"PASSWORDLESS_LOGIN"` | [recipe/passwordless/types.ts:434](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L434) | -| `urlWithLinkCode?` | `string` | [recipe/passwordless/types.ts:438](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L438) | -| `userInputCode?` | `string` | [recipe/passwordless/types.ts:437](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L437) | - -*** - -### TypePasswordlessSmsDeliveryInput - -```ts -type TypePasswordlessSmsDeliveryInput = object; -``` - -Defined in: [recipe/passwordless/types.ts:444](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L444) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `codeLifetime` | `number` | [recipe/passwordless/types.ts:450](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L450) | -| `isFirstFactor` | `boolean` | [recipe/passwordless/types.ts:446](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L446) | -| `phoneNumber` | `string` | [recipe/passwordless/types.ts:447](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L447) | -| `preAuthSessionId` | `string` | [recipe/passwordless/types.ts:451](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L451) | -| `tenantId` | `string` | [recipe/passwordless/types.ts:452](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L452) | -| `type` | `"PASSWORDLESS_LOGIN"` | [recipe/passwordless/types.ts:445](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L445) | -| `urlWithLinkCode?` | `string` | [recipe/passwordless/types.ts:449](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L449) | -| `userInputCode?` | `string` | [recipe/passwordless/types.ts:448](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/types.ts#L448) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless.mdx deleted file mode 100644 index e6fb85532e..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-passwordless.mdx +++ /dev/null @@ -1,1246 +0,0 @@ ---- -title: supertokens-node/recipe/passwordless -page_title: Passwordless -sidebar_position: 3 -description: References documentation for the supertokens-node/recipe/passwordless package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# Passwordless - - -## Classes - -### default - -Defined in: [recipe/passwordless/index.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L31) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [recipe/passwordless/index.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L34) | -| `init` | `static` | (`config`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/passwordless/index.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L32) | - -#### Methods - -##### checkCode() - -```ts -static checkCode(input): 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"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:219](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L219) - -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 - -| Parameter | Type | -| ------ | ------ | -| `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() - -###### Call Signature - -```ts -static consumeCode(input): 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"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:77](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L77) - -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 - -| Parameter | Type | -| ------ | ------ | -| `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`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `failedCodeInputAttemptCount`: `number`; - `maximumCodeInputAttempts`: `number`; - `status`: `"INCORRECT_USER_INPUT_CODE_ERROR"` \| `"EXPIRED_USER_INPUT_CODE_ERROR"`; -\} - \| \{ - `status`: `"RESTART_FLOW_ERROR"`; -\}\> - -###### Call Signature - -```ts -static consumeCode(input): 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"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L114) - -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 - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `deviceId`: `string`; `preAuthSessionId`: `string`; `session`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userInputCode`: `string`; \} \| \{ `linkCode`: `string`; `preAuthSessionId`: `string`; `session`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -###### Returns - -`Promise`\< - \| \{ - `consumedDevice`: \{ - `email?`: `string`; - `failedCodeInputAttemptCount`: `number`; - `phoneNumber?`: `string`; - `preAuthSessionId`: `string`; - \}; - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -static createCode(input): Promise<{ - codeId: string; - codeLifetime: number; - deviceId: string; - linkCode: string; - preAuthSessionId: string; - status: "OK"; - timeCreated: number; - userInputCode: string; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L36) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; \} \| \{ `phoneNumber`: `string`; \} & `object` | - -###### Returns - -`Promise`\<\{ - `codeId`: `string`; - `codeLifetime`: `number`; - `deviceId`: `string`; - `linkCode`: `string`; - `preAuthSessionId`: `string`; - `status`: `"OK"`; - `timeCreated`: `number`; - `userInputCode`: `string`; -\}\> - -##### createMagicLink() - -```ts -static createMagicLink(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:331](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L331) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -###### Returns - -`Promise`\<`string`\> - -##### createNewCodeForDevice() - -```ts -static createNewCodeForDevice(input): 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"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:59](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L59) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `deviceId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userInputCode?`: `string`; \} | -| `input.deviceId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.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() - -```ts -static listCodesByDeviceId(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:313](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L313) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `deviceId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.deviceId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)\> - -##### listCodesByEmail() - -```ts -static listCodesByEmail(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:299](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L299) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)[]\> - -##### listCodesByPhoneNumber() - -```ts -static listCodesByPhoneNumber(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:306](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L306) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.phoneNumber` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)[]\> - -##### listCodesByPreAuthSessionId() - -```ts -static listCodesByPreAuthSessionId(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:320](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L320) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `preAuthSessionId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.preAuthSessionId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)\> - -##### revokeAllCodes() - -```ts -static revokeAllCodes(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:269](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L269) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### revokeCode() - -```ts -static revokeCode(input): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:288](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L288) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `codeId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `preAuthSessionId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### sendEmail() - -```ts -static sendEmail(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:373](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L373) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypePasswordlessEmailDeliveryInput`](recipe-passwordless-types.mdx#typepasswordlessemaildeliveryinput) & `object` | - -###### Returns - -`Promise`\<`void`\> - -##### sendSms() - -```ts -static sendSms(input): Promise; -``` - -Defined in: [recipe/passwordless/index.ts:380](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L380) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypePasswordlessSmsDeliveryInput`](recipe-passwordless-types.mdx#typepasswordlesssmsdeliveryinput) & `object` | - -###### Returns - -`Promise`\<`void`\> - -##### signInUp() - -```ts -static signInUp(input): Promise<{ - createdNewRecipeUser: boolean; - recipeUserId: RecipeUserId; - status: string; - user: User; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:352](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L352) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `session?`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `session?`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -###### Returns - -`Promise`\<\{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `string`; - `user`: [`User`](types.mdx#user); -\}\> - -##### updateUser() - -```ts -static updateUser(input): 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"; -}>; -``` - -Defined in: [recipe/passwordless/index.ts:257](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L257) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email?`: `string`; `phoneNumber?`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email?` | `string` | -| `input.phoneNumber?` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.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"`; -\}\> - -## Variables - -### checkCode() - -```ts -checkCode: (input) => 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"; -}> = Wrapper.checkCode; -``` - -Defined in: [recipe/passwordless/index.ts:416](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L416) - -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 - -| Parameter | Type | -| ------ | ------ | -| `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() - -```ts -consumeCode: { - (input): 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"; -}>; - (input): 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"; -}>; -} = Wrapper.consumeCode; -``` - -Defined in: [recipe/passwordless/index.ts:394](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L394) - -#### Call Signature - -```ts -(input): 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"; -}>; -``` - -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 - -| Parameter | Type | -| ------ | ------ | -| `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`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `failedCodeInputAttemptCount`: `number`; - `maximumCodeInputAttempts`: `number`; - `status`: `"INCORRECT_USER_INPUT_CODE_ERROR"` \| `"EXPIRED_USER_INPUT_CODE_ERROR"`; -\} - \| \{ - `status`: `"RESTART_FLOW_ERROR"`; -\}\> - -#### Call Signature - -```ts -(input): 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"; -}>; -``` - -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 - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `deviceId`: `string`; `preAuthSessionId`: `string`; `session`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userInputCode`: `string`; \} \| \{ `linkCode`: `string`; `preAuthSessionId`: `string`; `session`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -##### Returns - -`Promise`\< - \| \{ - `consumedDevice`: \{ - `email?`: `string`; - `failedCodeInputAttemptCount`: `number`; - `phoneNumber?`: `string`; - `preAuthSessionId`: `string`; - \}; - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -createCode: (input) => Promise<{ - codeId: string; - codeLifetime: number; - deviceId: string; - linkCode: string; - preAuthSessionId: string; - status: "OK"; - timeCreated: number; - userInputCode: string; -}> = Wrapper.createCode; -``` - -Defined in: [recipe/passwordless/index.ts:392](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L392) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; \} \| \{ `phoneNumber`: `string`; \} & `object` | - -#### Returns - -`Promise`\<\{ - `codeId`: `string`; - `codeLifetime`: `number`; - `deviceId`: `string`; - `linkCode`: `string`; - `preAuthSessionId`: `string`; - `status`: `"OK"`; - `timeCreated`: `number`; - `userInputCode`: `string`; -\}\> - -*** - -### createMagicLink() - -```ts -createMagicLink: (input) => Promise = Wrapper.createMagicLink; -``` - -Defined in: [recipe/passwordless/index.ts:412](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L412) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -#### Returns - -`Promise`\<`string`\> - -*** - -### createNewCodeForDevice() - -```ts -createNewCodeForDevice: (input) => 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"; -}> = Wrapper.createNewCodeForDevice; -``` - -Defined in: [recipe/passwordless/index.ts:404](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L404) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `deviceId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userInputCode?`: `string`; \} | -| `input.deviceId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.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"`; -\}\> - -*** - -### Error - -```ts -Error: typeof default = Wrapper.Error; -``` - -Defined in: [recipe/passwordless/index.ts:390](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L390) - -*** - -### init() - -```ts -init: (config) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/passwordless/index.ts:388](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L388) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config` | [`TypeInput`](recipe-passwordless-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### listCodesByDeviceId() - -```ts -listCodesByDeviceId: (input) => Promise = Wrapper.listCodesByDeviceId; -``` - -Defined in: [recipe/passwordless/index.ts:396](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L396) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `deviceId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.deviceId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)\> - -*** - -### listCodesByEmail() - -```ts -listCodesByEmail: (input) => Promise = Wrapper.listCodesByEmail; -``` - -Defined in: [recipe/passwordless/index.ts:398](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L398) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)[]\> - -*** - -### listCodesByPhoneNumber() - -```ts -listCodesByPhoneNumber: (input) => Promise = Wrapper.listCodesByPhoneNumber; -``` - -Defined in: [recipe/passwordless/index.ts:400](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L400) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.phoneNumber` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)[]\> - -*** - -### listCodesByPreAuthSessionId() - -```ts -listCodesByPreAuthSessionId: (input) => Promise = Wrapper.listCodesByPreAuthSessionId; -``` - -Defined in: [recipe/passwordless/index.ts:402](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L402) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `preAuthSessionId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.preAuthSessionId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`DeviceType`](recipe-passwordless-types.mdx#devicetype)\> - -*** - -### revokeAllCodes() - -```ts -revokeAllCodes: (input) => Promise<{ - status: "OK"; -}> = Wrapper.revokeAllCodes; -``` - -Defined in: [recipe/passwordless/index.ts:408](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L408) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### revokeCode() - -```ts -revokeCode: (input) => Promise<{ - status: "OK"; -}> = Wrapper.revokeCode; -``` - -Defined in: [recipe/passwordless/index.ts:410](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L410) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `codeId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `preAuthSessionId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### sendEmail() - -```ts -sendEmail: (input) => Promise = Wrapper.sendEmail; -``` - -Defined in: [recipe/passwordless/index.ts:420](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L420) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypePasswordlessEmailDeliveryInput`](recipe-passwordless-types.mdx#typepasswordlessemaildeliveryinput) & `object` | - -#### Returns - -`Promise`\<`void`\> - -*** - -### sendSms() - -```ts -sendSms: (input) => Promise = Wrapper.sendSms; -``` - -Defined in: [recipe/passwordless/index.ts:422](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L422) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypePasswordlessSmsDeliveryInput`](recipe-passwordless-types.mdx#typepasswordlesssmsdeliveryinput) & `object` | - -#### Returns - -`Promise`\<`void`\> - -*** - -### signInUp() - -```ts -signInUp: (input) => Promise<{ - createdNewRecipeUser: boolean; - recipeUserId: RecipeUserId; - status: string; - user: User; -}> = Wrapper.signInUp; -``` - -Defined in: [recipe/passwordless/index.ts:414](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L414) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \| \{ `email`: `string`; `session?`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} \| \{ `phoneNumber`: `string`; `session?`: `SessionContainerInterface`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | - -#### Returns - -`Promise`\<\{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `string`; - `user`: [`User`](types.mdx#user); -\}\> - -*** - -### updateUser() - -```ts -updateUser: (input) => 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"; -}> = Wrapper.updateUser; -``` - -Defined in: [recipe/passwordless/index.ts:406](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/passwordless/index.ts#L406) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email?`: `string`; `phoneNumber?`: `string`; `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email?` | `string` | -| `input.phoneNumber?` | `string` | -| `input.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) | -| `input.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"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-passwordless-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-passwordless-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-passwordless-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty-types.mdx deleted file mode 100644 index 59d9a218db..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty-types.mdx +++ /dev/null @@ -1,443 +0,0 @@ ---- -title: supertokens-node/recipe/thirdparty/types -page_title: ThirdParty Types -sidebar_position: 4 -description: References documentation for the supertokens-node/recipe/thirdparty/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# ThirdParty Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/thirdparty/types.ts:253](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L253) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appleRedirectHandlerPOST` | `undefined` \| (`input`) => `Promise`\<`void`\> | [recipe/thirdparty/types.ts:313](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L313) | -| `authorisationUrlGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `pkceCodeVerifier?`: `string`; `status`: `"OK"`; `urlWithQueryParams`: `string`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/thirdparty/types.ts:254](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L254) | -| `signInUpPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `createdNewRecipeUser`: `boolean`; `oAuthTokens`: \{ \[`key`: `string`\]: `any`; \}; `rawUserInfoFromProvider`: \{ `fromIdTokenPayload?`: \{ \[`key`: `string`\]: `any`; \}; `fromUserInfoAPI?`: \{ \[`key`: `string`\]: `any`; \}; \}; `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| \{ `status`: `"NO_EMAIL_GIVEN_BY_PROVIDER"`; \} \| \{ `reason`: `string`; `status`: `"SIGN_IN_UP_NOT_ALLOWED"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/thirdparty/types.ts:271](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L271) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/thirdparty/types.ts:242](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L242) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/thirdparty/types.ts:250](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L250) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/thirdparty/types.ts:244](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L244) | -| `isInServerlessEnv` | `boolean` | [recipe/thirdparty/types.ts:246](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L246) | -| `providers` | [`ProviderInput`](#providerinput)[] | [recipe/thirdparty/types.ts:247](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L247) | -| `recipeId` | `string` | [recipe/thirdparty/types.ts:245](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L245) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/thirdparty/types.ts:243](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L243) | -| `req` | `BaseRequest` | [recipe/thirdparty/types.ts:248](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L248) | -| `res` | `BaseResponse` | [recipe/thirdparty/types.ts:249](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L249) | - -*** - -### ProviderClientConfig - -```ts -type ProviderClientConfig = object; -``` - -Defined in: [recipe/thirdparty/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L42) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `additionalConfig?` | `object` | [recipe/thirdparty/types.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L48) | -| `clientId` | `string` | [recipe/thirdparty/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L44) | -| `clientSecret?` | `string` | [recipe/thirdparty/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L45) | -| `clientType?` | `string` | [recipe/thirdparty/types.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L43) | -| `forcePKCE?` | `boolean` | [recipe/thirdparty/types.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L47) | -| `scope?` | `string`[] | [recipe/thirdparty/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L46) | - -*** - -### ProviderConfig - -```ts -type ProviderConfig = CommonProviderConfig & object; -``` - -Defined in: [recipe/thirdparty/types.ts:119](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L119) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `clients?` | [`ProviderClientConfig`](#providerclientconfig)[] | [recipe/thirdparty/types.ts:120](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L120) | - -*** - -### ProviderConfigForClientType - -```ts -type ProviderConfigForClientType = ProviderClientConfig & CommonProviderConfig; -``` - -Defined in: [recipe/thirdparty/types.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L94) - -*** - -### ProviderInput - -```ts -type ProviderInput = object; -``` - -Defined in: [recipe/thirdparty/types.ts:123](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L123) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`ProviderConfig`](#providerconfig) | [recipe/thirdparty/types.ts:124](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L124) | -| `includeInNonPublicTenantsByDefault?` | `boolean` | [recipe/thirdparty/types.ts:125](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L125) | -| `override?` | (`originalImplementation`) => [`TypeProvider`](#typeprovider) | [recipe/thirdparty/types.ts:126](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L126) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/thirdparty/types.ts:159](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L159) - -#### Methods - -##### getProvider() - -```ts -getProvider(input): Promise; -``` - -Defined in: [recipe/thirdparty/types.ts:160](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L160) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `clientType?`: `string`; `tenantId`: `string`; `thirdPartyId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.clientType?` | `string` | -| `input.tenantId` | `string` | -| `input.thirdPartyId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<[`TypeProvider`](#typeprovider)\> - -##### manuallyCreateOrUpdateUser() - -```ts -manuallyCreateOrUpdateUser(input): 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"; -}>; -``` - -Defined in: [recipe/thirdparty/types.ts:207](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L207) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `isVerified`: `boolean`; `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `thirdPartyId`: `string`; `thirdPartyUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.isVerified` | `boolean` | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.thirdPartyId` | `string` | -| `input.thirdPartyUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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() - -```ts -signInUp(input): Promise< - | { - createdNewRecipeUser: boolean; - oAuthTokens: { - [key: string]: any; - }; - rawUserInfoFromProvider: { - fromIdTokenPayload?: { - [key: string]: any; - }; - fromUserInfoAPI?: { - [key: string]: any; - }; - }; - 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"; -}>; -``` - -Defined in: [recipe/thirdparty/types.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L167) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `isVerified`: `boolean`; `oAuthTokens`: \{ \[`key`: `string`\]: `any`; \}; `rawUserInfoFromProvider`: \{ `fromIdTokenPayload?`: \{ \[`key`: `string`\]: `any`; \}; `fromUserInfoAPI?`: \{ \[`key`: `string`\]: `any`; \}; \}; `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `thirdPartyId`: `string`; `thirdPartyUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.isVerified` | `boolean` | -| `input.oAuthTokens` | \{ \[`key`: `string`\]: `any`; \} | -| `input.rawUserInfoFromProvider` | \{ `fromIdTokenPayload?`: \{ \[`key`: `string`\]: `any`; \}; `fromUserInfoAPI?`: \{ \[`key`: `string`\]: `any`; \}; \} | -| `input.rawUserInfoFromProvider.fromIdTokenPayload?` | \{ \[`key`: `string`\]: `any`; \} | -| `input.rawUserInfoFromProvider.fromUserInfoAPI?` | \{ \[`key`: `string`\]: `any`; \} | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.thirdPartyId` | `string` | -| `input.thirdPartyUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `oAuthTokens`: \{ - \[`key`: `string`\]: `any`; - \}; - `rawUserInfoFromProvider`: \{ - `fromIdTokenPayload?`: \{ - \[`key`: `string`\]: `any`; - \}; - `fromUserInfoAPI?`: \{ - \[`key`: `string`\]: `any`; - \}; - \}; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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"`; -\}\> - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/thirdparty/types.ts:137](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L137) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override?` | `object` | [recipe/thirdparty/types.ts:139](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L139) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/thirdparty/types.ts:144](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L144) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/thirdparty/types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L140) | -| `signInAndUpFeature?` | [`TypeInputSignInAndUp`](#typeinputsigninandup) | [recipe/thirdparty/types.ts:138](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L138) | - -*** - -### TypeInputSignInAndUp - -```ts -type TypeInputSignInAndUp = object; -``` - -Defined in: [recipe/thirdparty/types.ts:129](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L129) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `providers?` | [`ProviderInput`](#providerinput)[] | [recipe/thirdparty/types.ts:130](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L130) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/thirdparty/types.ts:148](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L148) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override` | `object` | [recipe/thirdparty/types.ts:150](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L150) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/thirdparty/types.ts:155](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L155) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/thirdparty/types.ts:151](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L151) | -| `signInAndUpFeature` | [`TypeNormalisedInputSignInAndUp`](#typenormalisedinputsigninandup) | [recipe/thirdparty/types.ts:149](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L149) | - -*** - -### TypeNormalisedInputSignInAndUp - -```ts -type TypeNormalisedInputSignInAndUp = object; -``` - -Defined in: [recipe/thirdparty/types.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L133) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `providers` | [`ProviderInput`](#providerinput)[] | [recipe/thirdparty/types.ts:134](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L134) | - -*** - -### TypeProvider - -```ts -type TypeProvider = object; -``` - -Defined in: [recipe/thirdparty/types.ts:96](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L96) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`ProviderConfigForClientType`](#providerconfigforclienttype) | [recipe/thirdparty/types.ts:98](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L98) | -| `exchangeAuthCodeForOAuthTokens` | (`input`) => `Promise`\<`any`\> | [recipe/thirdparty/types.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L108) | -| `getAuthorisationRedirectURL` | (`input`) => `Promise`\<\{ `pkceCodeVerifier?`: `string`; `urlWithQueryParams`: `string`; \}\> | [recipe/thirdparty/types.ts:104](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L104) | -| `getConfigForClientType` | (`input`) => `Promise`\<[`ProviderConfigForClientType`](#providerconfigforclienttype)\> | [recipe/thirdparty/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L100) | -| `getUserInfo` | (`input`) => `Promise`\<[`UserInfo`](#userinfo)\> | [recipe/thirdparty/types.ts:116](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L116) | -| `id` | `string` | [recipe/thirdparty/types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L97) | - -*** - -### UserInfo - -```ts -type UserInfo = object; -``` - -Defined in: [recipe/thirdparty/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L23) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `email?` | `object` | [recipe/thirdparty/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L25) | -| `email.id` | `string` | [recipe/thirdparty/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L25) | -| `email.isVerified` | `boolean` | [recipe/thirdparty/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L25) | -| `rawUserInfoFromProvider` | `object` | [recipe/thirdparty/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L26) | -| `rawUserInfoFromProvider.fromIdTokenPayload?` | `object` | [recipe/thirdparty/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L26) | -| `rawUserInfoFromProvider.fromUserInfoAPI?` | `object` | [recipe/thirdparty/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L26) | -| `thirdPartyUserId` | `string` | [recipe/thirdparty/types.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L24) | - -*** - -### UserInfoMap - -```ts -type UserInfoMap = object; -``` - -Defined in: [recipe/thirdparty/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L29) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `fromIdTokenPayload?` | `object` | [recipe/thirdparty/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L30) | -| `fromIdTokenPayload.email?` | `string` | [recipe/thirdparty/types.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L32) | -| `fromIdTokenPayload.emailVerified?` | `string` | [recipe/thirdparty/types.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L33) | -| `fromIdTokenPayload.userId?` | `string` | [recipe/thirdparty/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L31) | -| `fromUserInfoAPI?` | `object` | [recipe/thirdparty/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L35) | -| `fromUserInfoAPI.email?` | `string` | [recipe/thirdparty/types.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L37) | -| `fromUserInfoAPI.emailVerified?` | `string` | [recipe/thirdparty/types.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L38) | -| `fromUserInfoAPI.userId?` | `string` | [recipe/thirdparty/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/types.ts#L36) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty.mdx deleted file mode 100644 index 3658f31a3e..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-thirdparty.mdx +++ /dev/null @@ -1,452 +0,0 @@ ---- -title: supertokens-node/recipe/thirdparty -page_title: ThirdParty -sidebar_position: 4 -description: References documentation for the supertokens-node/recipe/thirdparty package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# ThirdParty - - -## Classes - -### default - -Defined in: [recipe/thirdparty/index.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L25) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [recipe/thirdparty/index.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L28) | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/thirdparty/index.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L26) | - -#### Methods - -##### getProvider() - -```ts -static getProvider( - tenantId, - thirdPartyId, - clientType, -userContext?): Promise; -``` - -Defined in: [recipe/thirdparty/index.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L30) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `clientType` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<[`TypeProvider`](recipe-thirdparty-types.mdx#typeprovider)\> - -##### manuallyCreateOrUpdateUser() - -###### Call Signature - -```ts -static manuallyCreateOrUpdateUser( - tenantId, - thirdPartyId, - thirdPartyUserId, - email, - isVerified, - session?, - 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"; -}>; -``` - -Defined in: [recipe/thirdparty/index.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L44) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `thirdPartyUserId` | `string` | -| `email` | `string` | -| `isVerified` | `boolean` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `reason`: `string`; - `status`: `"EMAIL_CHANGE_NOT_ALLOWED_ERROR"`; -\} - \| \{ - `reason`: `string`; - `status`: `"SIGN_IN_UP_NOT_ALLOWED"`; -\}\> - -###### Call Signature - -```ts -static manuallyCreateOrUpdateUser( - tenantId, - thirdPartyId, - thirdPartyUserId, - email, - isVerified, - session, - 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"; -}>; -``` - -Defined in: [recipe/thirdparty/index.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L68) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `thirdPartyUserId` | `string` | -| `email` | `string` | -| `isVerified` | `boolean` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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"`; -\}\> - -## Variables - -### Error - -```ts -Error: typeof default = Wrapper.Error; -``` - -Defined in: [recipe/thirdparty/index.ts:147](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L147) - -*** - -### getProvider() - -```ts -getProvider: (tenantId, thirdPartyId, clientType, userContext?) => Promise = Wrapper.getProvider; -``` - -Defined in: [recipe/thirdparty/index.ts:149](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L149) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `clientType` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<[`TypeProvider`](recipe-thirdparty-types.mdx#typeprovider)\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/thirdparty/index.ts:145](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L145) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-thirdparty-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### manuallyCreateOrUpdateUser() - -```ts -manuallyCreateOrUpdateUser: { - (tenantId, thirdPartyId, thirdPartyUserId, email, isVerified, session?, 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"; -}>; - (tenantId, thirdPartyId, thirdPartyUserId, email, isVerified, session, 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"; -}>; -} = Wrapper.manuallyCreateOrUpdateUser; -``` - -Defined in: [recipe/thirdparty/index.ts:151](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/thirdparty/index.ts#L151) - -#### Call Signature - -```ts -( - tenantId, - thirdPartyId, - thirdPartyUserId, - email, - isVerified, - session?, - 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"; -}>; -``` - -##### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `thirdPartyUserId` | `string` | -| `email` | `string` | -| `isVerified` | `boolean` | -| `session?` | `undefined` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| \{ - `reason`: `string`; - `status`: `"EMAIL_CHANGE_NOT_ALLOWED_ERROR"`; -\} - \| \{ - `reason`: `string`; - `status`: `"SIGN_IN_UP_NOT_ALLOWED"`; -\}\> - -#### Call Signature - -```ts -( - tenantId, - thirdPartyId, - thirdPartyUserId, - email, - isVerified, - session, - 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 - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `thirdPartyId` | `string` | -| `thirdPartyUserId` | `string` | -| `email` | `string` | -| `isVerified` | `boolean` | -| `session` | `SessionContainerInterface` | -| `userContext?` | `Record`\<`string`, `any`\> | - -##### Returns - -`Promise`\< - \| \{ - `createdNewRecipeUser`: `boolean`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#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"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-thirdparty-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-thirdparty-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-thirdparty-types.mdx#recipeinterface) - -*** - -### TypeProvider - -Re-exports [TypeProvider](recipe-thirdparty-types.mdx#typeprovider) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-totp-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-totp-types.mdx deleted file mode 100644 index 9f8b733cc6..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-totp-types.mdx +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: supertokens-node/recipe/totp/types -page_title: TOTP Types -sidebar_position: 16 -description: References documentation for the supertokens-node/recipe/totp/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# TOTP Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/totp/types.ts:149](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L149) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `createDevicePOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `deviceName`: `string`; `qrCodeString`: `string`; `secret`: `string`; `status`: `"OK"`; \} \| \{ `status`: `"DEVICE_ALREADY_EXISTS_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/totp/types.ts:150](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L150) | -| `listDevicesGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `devices`: `object`[]; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/totp/types.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L170) | -| `removeDevicePOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `didDeviceExist`: `boolean`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/totp/types.ts:185](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L185) | -| `verifyDevicePOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; `wasAlreadyVerified`: `boolean`; \} \| \{ `status`: `"UNKNOWN_DEVICE_ERROR"`; \} \| \{ `currentNumberOfFailedAttempts`: `number`; `maxNumberOfFailedAttempts`: `number`; `status`: `"INVALID_TOTP_ERROR"`; \} \| \{ `retryAfterMs`: `number`; `status`: `"LIMIT_REACHED_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/totp/types.ts:200](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L200) | -| `verifyTOTPPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"` \| `"UNKNOWN_USER_ID_ERROR"`; \} \| \{ `currentNumberOfFailedAttempts`: `number`; `maxNumberOfFailedAttempts`: `number`; `status`: `"INVALID_TOTP_ERROR"`; \} \| \{ `retryAfterMs`: `number`; `status`: `"LIMIT_REACHED_ERROR"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/totp/types.ts:228](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L228) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/totp/types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L140) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/totp/types.ts:142](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L142) | -| `isInServerlessEnv` | `boolean` | [recipe/totp/types.ts:144](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L144) | -| `recipeId` | `string` | [recipe/totp/types.ts:143](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L143) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/totp/types.ts:141](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L141) | -| `req` | `BaseRequest` | [recipe/totp/types.ts:145](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L145) | -| `res` | `BaseResponse` | [recipe/totp/types.ts:146](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L146) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/totp/types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L49) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `createDevice` | (`input`) => `Promise`\< \| \{ `deviceName`: `string`; `qrCodeString`: `string`; `secret`: `string`; `status`: `"OK"`; \} \| \{ `status`: `"DEVICE_ALREADY_EXISTS_ERROR"`; \} \| \{ `status`: `"UNKNOWN_USER_ID_ERROR"`; \}\> | [recipe/totp/types.ts:58](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L58) | -| `getUserIdentifierInfoForUserId` | (`input`) => `Promise`\< \| \{ `info`: `string`; `status`: `"OK"`; \} \| \{ `status`: `"UNKNOWN_USER_ID_ERROR"` \| `"USER_IDENTIFIER_INFO_DOES_NOT_EXIST_ERROR"`; \}\> | [recipe/totp/types.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L50) | -| `listDevices` | (`input`) => `Promise`\<\{ `devices`: `object`[]; `status`: `"OK"`; \}\> | [recipe/totp/types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L87) | -| `removeDevice` | (`input`) => `Promise`\<\{ `didDeviceExist`: `boolean`; `status`: `"OK"`; \}\> | [recipe/totp/types.ts:96](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L96) | -| `updateDevice` | (`input`) => `Promise`\<\{ `status`: `"OK"` \| `"UNKNOWN_DEVICE_ERROR"` \| `"DEVICE_ALREADY_EXISTS_ERROR"`; \}\> | [recipe/totp/types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L79) | -| `verifyDevice` | (`input`) => `Promise`\< \| \{ `status`: `"OK"`; `wasAlreadyVerified`: `boolean`; \} \| \{ `status`: `"UNKNOWN_DEVICE_ERROR"`; \} \| \{ `currentNumberOfFailedAttempts`: `number`; `maxNumberOfFailedAttempts`: `number`; `status`: `"INVALID_TOTP_ERROR"`; \} \| \{ `retryAfterMs`: `number`; `status`: `"LIMIT_REACHED_ERROR"`; \}\> | [recipe/totp/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L100) | -| `verifyTOTP` | (`input`) => `Promise`\< \| \{ `status`: `"OK"` \| `"UNKNOWN_USER_ID_ERROR"`; \} \| \{ `currentNumberOfFailedAttempts`: `number`; `maxNumberOfFailedAttempts`: `number`; `status`: `"INVALID_TOTP_ERROR"`; \} \| \{ `retryAfterMs`: `number`; `status`: `"LIMIT_REACHED_ERROR"`; \}\> | [recipe/totp/types.ts:124](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L124) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/totp/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L21) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `defaultPeriod?` | `number` | [recipe/totp/types.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L24) | -| `defaultSkew?` | `number` | [recipe/totp/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L23) | -| `issuer?` | `string` | [recipe/totp/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L22) | -| `override?` | `object` | [recipe/totp/types.ts:26](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L26) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/totp/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L31) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/totp/types.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L27) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/totp/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L35) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `defaultPeriod` | `number` | [recipe/totp/types.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L38) | -| `defaultSkew` | `number` | [recipe/totp/types.ts:37](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L37) | -| `issuer` | `string` | [recipe/totp/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L36) | -| `override` | `object` | [recipe/totp/types.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L40) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/totp/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L45) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/totp/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/types.ts#L41) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-totp.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-totp.mdx deleted file mode 100644 index faea961bc5..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-totp.mdx +++ /dev/null @@ -1,555 +0,0 @@ ---- -title: supertokens-node/recipe/totp -page_title: TOTP -sidebar_position: 16 -description: References documentation for the supertokens-node/recipe/totp package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# TOTP - - -## Classes - -### default - -Defined in: [recipe/totp/index.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L20) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/totp/index.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L21) | - -#### Methods - -##### createDevice() - -```ts -static createDevice( - userId, - userIdentifierInfo?, - deviceName?, - skew?, - period?, - userContext?): Promise< - | { - deviceName: string; - qrCodeString: string; - secret: string; - status: "OK"; -} - | { - status: "DEVICE_ALREADY_EXISTS_ERROR"; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}>; -``` - -Defined in: [recipe/totp/index.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L23) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userIdentifierInfo?` | `string` | -| `deviceName?` | `string` | -| `skew?` | `number` | -| `period?` | `number` | -| `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() - -```ts -static listDevices(userId, userContext?): Promise<{ - devices: object[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/totp/index.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L70) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `devices`: `object`[]; - `status`: `"OK"`; -\}\> - -##### removeDevice() - -```ts -static removeDevice( - userId, - deviceName, - userContext?): Promise<{ - didDeviceExist: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/totp/index.ts:88](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L88) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `deviceName` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `didDeviceExist`: `boolean`; - `status`: `"OK"`; -\}\> - -##### updateDevice() - -```ts -static updateDevice( - userId, - existingDeviceName, - newDeviceName, - userContext?): Promise<{ - status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR"; -}>; -``` - -Defined in: [recipe/totp/index.ts:54](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L54) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `existingDeviceName` | `string` | -| `newDeviceName` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"DEVICE_ALREADY_EXISTS_ERROR"` \| `"UNKNOWN_DEVICE_ERROR"`; -\}\> - -##### verifyDevice() - -```ts -static verifyDevice( - tenantId, - userId, - deviceName, - totp, - userContext?): Promise< - | { - status: "OK"; - wasAlreadyVerified: boolean; -} - | { - status: "UNKNOWN_DEVICE_ERROR"; -} - | { - currentNumberOfFailedAttempts: number; - maxNumberOfFailedAttempts: number; - status: "INVALID_TOTP_ERROR"; -} - | { - retryAfterMs: number; - status: "LIMIT_REACHED_ERROR"; -}>; -``` - -Defined in: [recipe/totp/index.ts:103](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L103) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `deviceName` | `string` | -| `totp` | `string` | -| `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() - -```ts -static verifyTOTP( - tenantId, - userId, - totp, - userContext?): Promise< - | { - status: "OK" | "UNKNOWN_USER_ID_ERROR"; -} - | { - currentNumberOfFailedAttempts: number; - maxNumberOfFailedAttempts: number; - status: "INVALID_TOTP_ERROR"; -} - | { - retryAfterMs: number; - status: "LIMIT_REACHED_ERROR"; -}>; -``` - -Defined in: [recipe/totp/index.ts:136](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L136) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `totp` | `string` | -| `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"`; -\}\> - -## Variables - -### createDevice() - -```ts -createDevice: (userId, userIdentifierInfo?, deviceName?, skew?, period?, userContext?) => Promise< - | { - deviceName: string; - qrCodeString: string; - secret: string; - status: "OK"; -} - | { - status: "DEVICE_ALREADY_EXISTS_ERROR"; -} - | { - status: "UNKNOWN_USER_ID_ERROR"; -}> = Wrapper.createDevice; -``` - -Defined in: [recipe/totp/index.ts:166](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L166) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userIdentifierInfo?` | `string` | -| `deviceName?` | `string` | -| `skew?` | `number` | -| `period?` | `number` | -| `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() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/totp/index.ts:164](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L164) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-totp-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### listDevices() - -```ts -listDevices: (userId, userContext?) => Promise<{ - devices: object[]; - status: "OK"; -}> = Wrapper.listDevices; -``` - -Defined in: [recipe/totp/index.ts:167](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L167) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `devices`: `object`[]; - `status`: `"OK"`; -\}\> - -*** - -### removeDevice() - -```ts -removeDevice: (userId, deviceName, userContext?) => Promise<{ - didDeviceExist: boolean; - status: "OK"; -}> = Wrapper.removeDevice; -``` - -Defined in: [recipe/totp/index.ts:169](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L169) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `deviceName` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `didDeviceExist`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### updateDevice() - -```ts -updateDevice: (userId, existingDeviceName, newDeviceName, userContext?) => Promise<{ - status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR"; -}> = Wrapper.updateDevice; -``` - -Defined in: [recipe/totp/index.ts:168](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L168) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `existingDeviceName` | `string` | -| `newDeviceName` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"DEVICE_ALREADY_EXISTS_ERROR"` \| `"UNKNOWN_DEVICE_ERROR"`; -\}\> - -*** - -### verifyDevice() - -```ts -verifyDevice: (tenantId, userId, deviceName, totp, userContext?) => Promise< - | { - status: "OK"; - wasAlreadyVerified: boolean; -} - | { - status: "UNKNOWN_DEVICE_ERROR"; -} - | { - currentNumberOfFailedAttempts: number; - maxNumberOfFailedAttempts: number; - status: "INVALID_TOTP_ERROR"; -} - | { - retryAfterMs: number; - status: "LIMIT_REACHED_ERROR"; -}> = Wrapper.verifyDevice; -``` - -Defined in: [recipe/totp/index.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L170) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `deviceName` | `string` | -| `totp` | `string` | -| `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() - -```ts -verifyTOTP: (tenantId, userId, totp, userContext?) => Promise< - | { - status: "OK" | "UNKNOWN_USER_ID_ERROR"; -} - | { - currentNumberOfFailedAttempts: number; - maxNumberOfFailedAttempts: number; - status: "INVALID_TOTP_ERROR"; -} - | { - retryAfterMs: number; - status: "LIMIT_REACHED_ERROR"; -}> = Wrapper.verifyTOTP; -``` - -Defined in: [recipe/totp/index.ts:171](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/totp/index.ts#L171) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `totp` | `string` | -| `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"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-totp-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-totp-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-totp-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata-types.mdx deleted file mode 100644 index 845f814a2f..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata-types.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -title: supertokens-node/recipe/usermetadata/types -page_title: UserMetadata Types -sidebar_position: 15 -description: References documentation for the supertokens-node/recipe/usermetadata/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# UserMetadata Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/usermetadata/types.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L39) - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/usermetadata/types.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L41) - -#### Properties - -| Property | Type | Description | Defined in | -| ------ | ------ | ------ | ------ | -| `clearUserMetadata` | (`input`) => `Promise`\<\{ `status`: `"OK"`; \}\> | - | [recipe/usermetadata/types.ts:60](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L60) | -| `getUserMetadata` | (`input`) => `Promise`\<\{ `metadata`: `any`; `status`: `"OK"`; \}\> | - | [recipe/usermetadata/types.ts:42](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L42) | -| `updateUserMetadata` | (`input`) => `Promise`\<\{ `metadata`: [`JSONObject`](types.mdx#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.: - stored: `{ "preferences": { "theme":"dark" }, "notifications": { "email": true }, "todos": ["example"] }` - update: `{ "notifications": { "sms": true }, "todos": null }` - result: `{ "preferences": { "theme":"dark" }, "notifications": { "sms": true } }` | [recipe/usermetadata/types.ts:55](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L55) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/usermetadata/types.ts:19](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L19) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override?` | `object` | [recipe/usermetadata/types.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L20) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/usermetadata/types.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L25) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/usermetadata/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L21) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/usermetadata/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L29) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override` | `object` | [recipe/usermetadata/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L30) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/usermetadata/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L35) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/usermetadata/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/types.ts#L31) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata.mdx deleted file mode 100644 index bbb4781574..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-usermetadata.mdx +++ /dev/null @@ -1,228 +0,0 @@ ---- -title: supertokens-node/recipe/usermetadata -page_title: UserMetadata -sidebar_position: 15 -description: References documentation for the supertokens-node/recipe/usermetadata package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# UserMetadata - - -## Classes - -### default - -Defined in: [recipe/usermetadata/index.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L21) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/usermetadata/index.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L22) | - -#### Methods - -##### clearUserMetadata() - -```ts -static clearUserMetadata(userId, userContext?): Promise<{ - status: "OK"; -}>; -``` - -Defined in: [recipe/usermetadata/index.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L39) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -##### getUserMetadata() - -```ts -static getUserMetadata(userId, userContext?): Promise<{ - metadata: any; - status: "OK"; -}>; -``` - -Defined in: [recipe/usermetadata/index.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L24) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `metadata`: `any`; - `status`: `"OK"`; -\}\> - -##### updateUserMetadata() - -```ts -static updateUserMetadata( - userId, - metadataUpdate, - userContext?): Promise<{ - metadata: JSONObject; - status: "OK"; -}>; -``` - -Defined in: [recipe/usermetadata/index.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L31) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `metadataUpdate` | [`JSONObject`](types.mdx#jsonobject) | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `metadata`: [`JSONObject`](types.mdx#jsonobject); - `status`: `"OK"`; -\}\> - -## Variables - -### clearUserMetadata() - -```ts -const clearUserMetadata: (userId, userContext?) => Promise<{ - status: "OK"; -}> = Wrapper.clearUserMetadata; -``` - -Defined in: [recipe/usermetadata/index.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L50) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"`; -\}\> - -*** - -### getUserMetadata() - -```ts -const getUserMetadata: (userId, userContext?) => Promise<{ - metadata: any; - status: "OK"; -}> = Wrapper.getUserMetadata; -``` - -Defined in: [recipe/usermetadata/index.ts:48](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L48) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `metadata`: `any`; - `status`: `"OK"`; -\}\> - -*** - -### init() - -```ts -const init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/usermetadata/index.ts:47](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L47) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-usermetadata-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### updateUserMetadata() - -```ts -const updateUserMetadata: (userId, metadataUpdate, userContext?) => Promise<{ - metadata: JSONObject; - status: "OK"; -}> = Wrapper.updateUserMetadata; -``` - -Defined in: [recipe/usermetadata/index.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/usermetadata/index.ts#L49) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userId` | `string` | -| `metadataUpdate` | [`JSONObject`](types.mdx#jsonobject) | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `metadata`: [`JSONObject`](types.mdx#jsonobject); - `status`: `"OK"`; -\}\> - -## References - -### JSONObject - -Re-exports [JSONObject](types.mdx#jsonobject) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-usermetadata-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles-types.mdx deleted file mode 100644 index 05f20a40b3..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles-types.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: supertokens-node/recipe/userroles/types -page_title: UserRoles Types -sidebar_position: 14 -description: References documentation for the supertokens-node/recipe/userroles/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# UserRoles Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/userroles/types.ts:43](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L43) - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/userroles/types.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L45) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `addRoleToUser` | (`input`) => `Promise`\< \| \{ `didUserAlreadyHaveRole`: `boolean`; `status`: `"OK"`; \} \| \{ `status`: `"UNKNOWN_ROLE_ERROR"`; \}\> | [recipe/userroles/types.ts:46](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L46) | -| `createNewRoleOrAddPermissions` | (`input`) => `Promise`\<\{ `createdNewRole`: `boolean`; `status`: `"OK"`; \}\> | [recipe/userroles/types.ts:81](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L81) | -| `deleteRole` | (`input`) => `Promise`\<\{ `didRoleExist`: `boolean`; `status`: `"OK"`; \}\> | [recipe/userroles/types.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L109) | -| `getAllRoles` | (`input`) => `Promise`\<\{ `roles`: `string`[]; `status`: `"OK"`; \}\> | [recipe/userroles/types.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L114) | -| `getPermissionsForRole` | (`input`) => `Promise`\< \| \{ `permissions`: `string`[]; `status`: `"OK"`; \} \| \{ `status`: `"UNKNOWN_ROLE_ERROR"`; \}\> | [recipe/userroles/types.ts:90](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L90) | -| `getRolesForUser` | (`input`) => `Promise`\<\{ `roles`: `string`[]; `status`: `"OK"`; \}\> | [recipe/userroles/types.ts:66](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L66) | -| `getRolesThatHavePermission` | (`input`) => `Promise`\<\{ `roles`: `string`[]; `status`: `"OK"`; \}\> | [recipe/userroles/types.ts:104](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L104) | -| `getUsersThatHaveRole` | (`input`) => `Promise`\< \| \{ `status`: `"OK"`; `users`: `string`[]; \} \| \{ `status`: `"UNKNOWN_ROLE_ERROR"`; \}\> | [recipe/userroles/types.ts:71](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L71) | -| `removePermissionsFromRole` | (`input`) => `Promise`\<\{ `status`: `"OK"` \| `"UNKNOWN_ROLE_ERROR"`; \}\> | [recipe/userroles/types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L100) | -| `removeUserRole` | (`input`) => `Promise`\< \| \{ `didUserHaveRole`: `boolean`; `status`: `"OK"`; \} \| \{ `status`: `"UNKNOWN_ROLE_ERROR"`; \}\> | [recipe/userroles/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L56) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/userroles/types.ts:19](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L19) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override?` | `object` | [recipe/userroles/types.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L22) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/userroles/types.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L27) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/userroles/types.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L23) | -| `skipAddingPermissionsToAccessToken?` | `boolean` | [recipe/userroles/types.ts:21](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L21) | -| `skipAddingRolesToAccessToken?` | `boolean` | [recipe/userroles/types.ts:20](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L20) | - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/userroles/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L31) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `override` | `object` | [recipe/userroles/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L34) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/userroles/types.ts:39](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L39) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/userroles/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L35) | -| `skipAddingPermissionsToAccessToken` | `boolean` | [recipe/userroles/types.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L33) | -| `skipAddingRolesToAccessToken` | `boolean` | [recipe/userroles/types.ts:32](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/types.ts#L32) | diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles.mdx deleted file mode 100644 index 7485a282c8..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-userroles.mdx +++ /dev/null @@ -1,701 +0,0 @@ ---- -title: supertokens-node/recipe/userroles -page_title: UserRoles -sidebar_position: 14 -description: References documentation for the supertokens-node/recipe/userroles package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# UserRoles - - -## Classes - -### default - -Defined in: [recipe/userroles/index.ts:22](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L22) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/userroles/index.ts:23](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L23) | -| `PermissionClaim` | `static` | `PermissionClaimClass` | `PermissionClaim` | [recipe/userroles/index.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L24) | -| `UserRoleClaim` | `static` | `UserRoleClaimClass` | `UserRoleClaim` | [recipe/userroles/index.ts:25](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L25) | - -#### Methods - -##### addRoleToUser() - -```ts -static addRoleToUser( - tenantId, - userId, - role, - userContext?): Promise< - | { - didUserAlreadyHaveRole: boolean; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L27) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `didUserAlreadyHaveRole`: `boolean`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -##### createNewRoleOrAddPermissions() - -```ts -static createNewRoleOrAddPermissions( - role, - permissions, - userContext?): Promise<{ - createdNewRole: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:61](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L61) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `permissions` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `createdNewRole`: `boolean`; - `status`: `"OK"`; -\}\> - -##### deleteRole() - -```ts -static deleteRole(role, userContext?): Promise<{ - didRoleExist: boolean; - status: "OK"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L91) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `didRoleExist`: `boolean`; - `status`: `"OK"`; -\}\> - -##### getAllRoles() - -```ts -static getAllRoles(userContext?): Promise<{ - roles: string[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:98](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L98) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -##### getPermissionsForRole() - -```ts -static getPermissionsForRole(role, userContext?): Promise< - | { - permissions: string[]; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L69) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `permissions`: `string`[]; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -##### getRolesForUser() - -```ts -static getRolesForUser( - tenantId, - userId, - userContext?): Promise<{ - roles: string[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:45](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L45) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -##### getRolesThatHavePermission() - -```ts -static getRolesThatHavePermission(permission, userContext?): Promise<{ - roles: string[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L84) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `permission` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -##### getUsersThatHaveRole() - -```ts -static getUsersThatHaveRole( - tenantId, - role, - userContext?): Promise< - | { - status: "OK"; - users: string[]; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:53](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L53) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `users`: `string`[]; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -##### removePermissionsFromRole() - -```ts -static removePermissionsFromRole( - role, - permissions, - userContext?): Promise<{ - status: "OK" | "UNKNOWN_ROLE_ERROR"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L76) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `permissions` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_ROLE_ERROR"`; -\}\> - -##### removeUserRole() - -```ts -static removeUserRole( - tenantId, - userId, - role, - userContext?): Promise< - | { - didUserHaveRole: boolean; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}>; -``` - -Defined in: [recipe/userroles/index.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L36) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `didUserHaveRole`: `boolean`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -## Variables - -### addRoleToUser() - -```ts -const addRoleToUser: (tenantId, userId, role, userContext?) => Promise< - | { - didUserAlreadyHaveRole: boolean; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}> = Wrapper.addRoleToUser; -``` - -Defined in: [recipe/userroles/index.ts:106](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L106) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `didUserAlreadyHaveRole`: `boolean`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -*** - -### createNewRoleOrAddPermissions() - -```ts -const createNewRoleOrAddPermissions: (role, permissions, userContext?) => Promise<{ - createdNewRole: boolean; - status: "OK"; -}> = Wrapper.createNewRoleOrAddPermissions; -``` - -Defined in: [recipe/userroles/index.ts:110](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L110) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `permissions` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `createdNewRole`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### deleteRole() - -```ts -const deleteRole: (role, userContext?) => Promise<{ - didRoleExist: boolean; - status: "OK"; -}> = Wrapper.deleteRole; -``` - -Defined in: [recipe/userroles/index.ts:114](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L114) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `didRoleExist`: `boolean`; - `status`: `"OK"`; -\}\> - -*** - -### getAllRoles() - -```ts -const getAllRoles: (userContext?) => Promise<{ - roles: string[]; - status: "OK"; -}> = Wrapper.getAllRoles; -``` - -Defined in: [recipe/userroles/index.ts:115](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L115) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -*** - -### getPermissionsForRole() - -```ts -const getPermissionsForRole: (role, userContext?) => Promise< - | { - permissions: string[]; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}> = Wrapper.getPermissionsForRole; -``` - -Defined in: [recipe/userroles/index.ts:111](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L111) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `permissions`: `string`[]; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -*** - -### getRolesForUser() - -```ts -const getRolesForUser: (tenantId, userId, userContext?) => Promise<{ - roles: string[]; - status: "OK"; -}> = Wrapper.getRolesForUser; -``` - -Defined in: [recipe/userroles/index.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L108) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -*** - -### getRolesThatHavePermission() - -```ts -const getRolesThatHavePermission: (permission, userContext?) => Promise<{ - roles: string[]; - status: "OK"; -}> = Wrapper.getRolesThatHavePermission; -``` - -Defined in: [recipe/userroles/index.ts:113](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L113) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `permission` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `roles`: `string`[]; - `status`: `"OK"`; -\}\> - -*** - -### getUsersThatHaveRole() - -```ts -const getUsersThatHaveRole: (tenantId, role, userContext?) => Promise< - | { - status: "OK"; - users: string[]; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}> = Wrapper.getUsersThatHaveRole; -``` - -Defined in: [recipe/userroles/index.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L109) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `users`: `string`[]; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -*** - -### init() - -```ts -const init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/userroles/index.ts:105](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L105) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-userroles-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### PermissionClaim - -```ts -const PermissionClaim: PermissionClaimClass; -``` - -Defined in: [recipe/userroles/permissionClaim.ts:41](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/permissionClaim.ts#L41) - -*** - -### removePermissionsFromRole() - -```ts -const removePermissionsFromRole: (role, permissions, userContext?) => Promise<{ - status: "OK" | "UNKNOWN_ROLE_ERROR"; -}> = Wrapper.removePermissionsFromRole; -``` - -Defined in: [recipe/userroles/index.ts:112](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L112) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `role` | `string` | -| `permissions` | `string`[] | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `status`: `"OK"` \| `"UNKNOWN_ROLE_ERROR"`; -\}\> - -*** - -### removeUserRole() - -```ts -const removeUserRole: (tenantId, userId, role, userContext?) => Promise< - | { - didUserHaveRole: boolean; - status: "OK"; -} - | { - status: "UNKNOWN_ROLE_ERROR"; -}> = Wrapper.removeUserRole; -``` - -Defined in: [recipe/userroles/index.ts:107](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/index.ts#L107) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `tenantId` | `string` | -| `userId` | `string` | -| `role` | `string` | -| `userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `didUserHaveRole`: `boolean`; - `status`: `"OK"`; -\} - \| \{ - `status`: `"UNKNOWN_ROLE_ERROR"`; -\}\> - -*** - -### UserRoleClaim - -```ts -const UserRoleClaim: UserRoleClaimClass; -``` - -Defined in: [recipe/userroles/userRoleClaim.ts:24](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/userroles/userRoleClaim.ts#L24) - -## References - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-userroles-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn-types.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn-types.mdx deleted file mode 100644 index a98166f022..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn-types.mdx +++ /dev/null @@ -1,1044 +0,0 @@ ---- -title: supertokens-node/recipe/webauthn/types -page_title: WebAuthn Types -sidebar_position: 5 -description: References documentation for the supertokens-node/recipe/webauthn/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# WebAuthn Types - - -## Type Aliases - -### APIInterface - -```ts -type APIInterface = object; -``` - -Defined in: [recipe/webauthn/types.ts:477](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L477) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailExistsGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `exists`: `boolean`; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/webauthn/types.ts:655](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L655) | -| `generateRecoverAccountTokenPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `GenerateRecoverAccountTokenPOSTErrorResponse`\> | [recipe/webauthn/types.ts:580](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L580) | -| `listCredentialsGET` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `credentials`: `object`[]; `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse)\> | [recipe/webauthn/types.ts:609](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L609) | -| `recoverAccountPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `email`: `string`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `RecoverAccountPOSTErrorResponse`\> | [recipe/webauthn/types.ts:590](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L590) | -| `registerCredentialPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `RegisterCredentialPOSTErrorResponse`\> | [recipe/webauthn/types.ts:623](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L623) | -| `registerOptionsPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `attestation`: `"none"` \| `"indirect"` \| `"direct"` \| `"enterprise"`; `authenticatorSelection`: \{ `requireResidentKey`: `boolean`; `residentKey`: [`ResidentKey`](#residentkey); `userVerification`: [`UserVerification`](#userverification); \}; `challenge`: `string`; `createdAt`: `number`; `excludeCredentials`: `object`[]; `expiresAt`: `number`; `pubKeyCredParams`: `object`[]; `rp`: \{ `id`: `string`; `name`: `string`; \}; `status`: `"OK"`; `timeout`: `number`; `user`: \{ `displayName`: `string`; `id`: `string`; `name`: `string`; \}; `webauthnGeneratedOptionsId`: `string`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `RegisterOptionsPOSTErrorResponse`\> | [recipe/webauthn/types.ts:478](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L478) | -| `removeCredentialPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `status`: `"OK"`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `RemoveCredentialPOSTErrorResponse`\> | [recipe/webauthn/types.ts:640](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L640) | -| `signInOptionsPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `challenge`: `string`; `createdAt`: `number`; `expiresAt`: `number`; `rpId`: `string`; `status`: `"OK"`; `timeout`: `number`; `userVerification`: [`UserVerification`](#userverification); `webauthnGeneratedOptionsId`: `string`; \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `SignInOptionsPOSTErrorResponse`\> | [recipe/webauthn/types.ts:523](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L523) | -| `signInPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `SignInPOSTErrorResponse`\> | [recipe/webauthn/types.ts:560](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L560) | -| `signUpPOST` | \| `undefined` \| (`input`) => `Promise`\< \| \{ `session`: `SessionContainerInterface`; `status`: `"OK"`; `user`: [`User`](types.mdx#user); \} \| [`GeneralErrorResponse`](types.mdx#generalerrorresponse) \| `SignUpPOSTErrorResponse`\> | [recipe/webauthn/types.ts:540](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L540) | - -*** - -### APIOptions - -```ts -type APIOptions = object; -``` - -Defined in: [recipe/webauthn/types.ts:420](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L420) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](types.mdx#normalisedappinfo) | [recipe/webauthn/types.ts:422](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L422) | -| `config` | [`TypeNormalisedInput`](#typenormalisedinput) | [recipe/webauthn/types.ts:423](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L423) | -| `emailDelivery` | `EmailDeliveryIngredient`\<[`TypeWebauthnEmailDeliveryInput`](#typewebauthnemaildeliveryinput)\> | [recipe/webauthn/types.ts:428](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L428) | -| `isInServerlessEnv` | `boolean` | [recipe/webauthn/types.ts:425](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L425) | -| `recipeId` | `string` | [recipe/webauthn/types.ts:424](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L424) | -| `recipeImplementation` | [`RecipeInterface`](#recipeinterface) | [recipe/webauthn/types.ts:421](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L421) | -| `req` | `BaseRequest` | [recipe/webauthn/types.ts:426](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L426) | -| `res` | `BaseResponse` | [recipe/webauthn/types.ts:427](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L427) | - -*** - -### Attestation - -```ts -type Attestation = "none" | "indirect" | "direct" | "enterprise"; -``` - -Defined in: [recipe/webauthn/types.ts:173](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L173) - -*** - -### AuthenticationPayload - -```ts -type AuthenticationPayload = CredentialPayloadBase & object; -``` - -Defined in: [recipe/webauthn/types.ts:703](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L703) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `response` | [`AuthenticatorAssertionResponseJSON`](#authenticatorassertionresponsejson) | [recipe/webauthn/types.ts:704](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L704) | - -*** - -### AuthenticatorAssertionResponseJSON - -```ts -type AuthenticatorAssertionResponseJSON = object; -``` - -Defined in: [recipe/webauthn/types.ts:687](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L687) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `authenticatorData` | `Base64URLString` | [recipe/webauthn/types.ts:689](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L689) | -| `clientDataJSON` | `Base64URLString` | [recipe/webauthn/types.ts:688](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L688) | -| `signature` | `Base64URLString` | [recipe/webauthn/types.ts:690](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L690) | -| `userHandle?` | `Base64URLString` | [recipe/webauthn/types.ts:691](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L691) | - -*** - -### AuthenticatorAttestationResponseJSON - -```ts -type AuthenticatorAttestationResponseJSON = object; -``` - -Defined in: [recipe/webauthn/types.ts:694](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L694) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `attestationObject` | `Base64URLString` | [recipe/webauthn/types.ts:696](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L696) | -| `authenticatorData?` | `Base64URLString` | [recipe/webauthn/types.ts:697](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L697) | -| `clientDataJSON` | `Base64URLString` | [recipe/webauthn/types.ts:695](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L695) | -| `publicKey?` | `Base64URLString` | [recipe/webauthn/types.ts:700](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L700) | -| `publicKeyAlgorithm?` | `COSEAlgorithmIdentifier` | [recipe/webauthn/types.ts:699](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L699) | -| `transports?` | (`"ble"` \| `"cable"` \| `"hybrid"` \| `"internal"` \| `"nfc"` \| `"smart-card"` \| `"usb"`)[] | [recipe/webauthn/types.ts:698](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L698) | - -*** - -### CredentialPayload - -```ts -type CredentialPayload = CredentialPayloadBase & object; -``` - -Defined in: [recipe/webauthn/types.ts:711](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L711) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `response` | `object` | [recipe/webauthn/types.ts:712](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L712) | -| `response.attestationObject` | `string` | [recipe/webauthn/types.ts:714](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L714) | -| `response.clientDataJSON` | `string` | [recipe/webauthn/types.ts:713](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L713) | -| `response.transports?` | (`"ble"` \| `"cable"` \| `"hybrid"` \| `"internal"` \| `"nfc"` \| `"smart-card"` \| `"usb"`)[] | [recipe/webauthn/types.ts:715](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L715) | -| `response.userHandle` | `string` | [recipe/webauthn/types.ts:716](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L716) | - -*** - -### CredentialPayloadBase - -```ts -type CredentialPayloadBase = object; -``` - -Defined in: [recipe/webauthn/types.ts:679](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L679) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `authenticatorAttachment?` | `"platform"` \| `"cross-platform"` | [recipe/webauthn/types.ts:682](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L682) | -| `clientExtensionResults` | `Record`\<`string`, `unknown`\> | [recipe/webauthn/types.ts:683](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L683) | -| `id` | `string` | [recipe/webauthn/types.ts:680](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L680) | -| `rawId` | `string` | [recipe/webauthn/types.ts:681](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L681) | -| `type` | `"public-key"` | [recipe/webauthn/types.ts:684](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L684) | - -*** - -### RecipeInterface - -```ts -type RecipeInterface = object; -``` - -Defined in: [recipe/webauthn/types.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L175) - -#### Methods - -##### consumeRecoverAccountToken() - -```ts -consumeRecoverAccountToken(input): Promise< - | { - email: string; - status: "OK"; - userId: string; -} -| ConsumeRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:322](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L322) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| `ConsumeRecoverAccountTokenErrorResponse`\> - -##### createNewRecipeUser() - -```ts -createNewRecipeUser(input): Promise< - | CreateNewRecipeUserErrorResponse - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -}>; -``` - -Defined in: [recipe/webauthn/types.ts:296](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L296) - -This function is meant only for creating the recipe in the core and nothing else. -We added this even though signUp exists cause devs may override signup expecting it -to be called just during sign up. But we also need a version of signing up which can be -called during operations like creating a user during account recovery flow. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](#registrationpayload); `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](#registrationpayload) | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| `CreateNewRecipeUserErrorResponse` - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\}\> - -##### generateRecoverAccountToken() - -```ts -generateRecoverAccountToken(input): Promise< - | { - status: "OK"; - token: string; -} -| GenerateRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:315](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L315) - -We pass in the email as well to this function cause the input userId -may not be associated with an webauthn account. In this case, we -need to know which email to use to create an webauthn account later on. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `userId`: `string`; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.userId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| `GenerateRecoverAccountTokenErrorResponse`\> - -##### getCredential() - -```ts -getCredential(input): Promise< - | { - createdAt: number; - recipeUserId: RecipeUserId; - relyingPartyId: string; - status: "OK"; - webauthnCredentialId: string; -} -| GetCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:358](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L358) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnCredentialId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `createdAt`: `number`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `relyingPartyId`: `string`; - `status`: `"OK"`; - `webauthnCredentialId`: `string`; -\} - \| `GetCredentialErrorResponse`\> - -##### getGeneratedOptions() - -```ts -getGeneratedOptions(input): Promise< - | { - challenge: string; - createdAt: number; - email?: string; - expiresAt: number; - origin: string; - relyingPartyId: string; - relyingPartyName: string; - status: "OK"; - timeout: number; - userPresence: boolean; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} -| GetGeneratedOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:385](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L385) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `email?`: `string`; - `expiresAt`: `number`; - `origin`: `string`; - `relyingPartyId`: `string`; - `relyingPartyName`: `string`; - `status`: `"OK"`; - `timeout`: `number`; - `userPresence`: `boolean`; - `userVerification`: [`UserVerification`](#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `GetGeneratedOptionsErrorResponse`\> - -##### getUserFromRecoverAccountToken() - -```ts -getUserFromRecoverAccountToken(input): Promise< - | { - recipeUserId?: RecipeUserId; - status: "OK"; - user: User; -} -| GetUserFromRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:343](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L343) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId?`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `GetUserFromRecoverAccountTokenErrorResponse`\> - -##### listCredentials() - -```ts -listCredentials(input): Promise<{ - credentials: object[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/webauthn/types.ts:369](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L369) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.recipeUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\<\{ - `credentials`: `object`[]; - `status`: `"OK"`; -\}\> - -##### registerCredential() - -```ts -registerCredential(input): Promise< - | { - status: "OK"; -} -| RegisterCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:331](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L331) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](#registrationpayload); `recipeUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](#registrationpayload) | -| `input.recipeUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RegisterCredentialErrorResponse`\> - -##### registerOptions() - -```ts -registerOptions(input): Promise< - | { - attestation: Attestation; - authenticatorSelection: { - requireResidentKey: boolean; - residentKey: ResidentKey; - userVerification: UserVerification; - }; - challenge: string; - createdAt: number; - excludeCredentials: object[]; - expiresAt: number; - pubKeyCredParams: object[]; - rp: { - id: string; - name: string; - }; - status: "OK"; - timeout: number; - user: { - displayName: string; - id: string; - name: string; - }; - webauthnGeneratedOptionsId: string; -} -| RegisterOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L176) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | `object` & \| \{ `recoverAccountToken`: `string`; \} \| \{ `displayName`: `string` \| `undefined`; `email`: `string`; \} | - -###### Returns - -`Promise`\< - \| \{ - `attestation`: [`Attestation`](#attestation); - `authenticatorSelection`: \{ - `requireResidentKey`: `boolean`; - `residentKey`: [`ResidentKey`](#residentkey); - `userVerification`: [`UserVerification`](#userverification); - \}; - `challenge`: `string`; - `createdAt`: `number`; - `excludeCredentials`: `object`[]; - `expiresAt`: `number`; - `pubKeyCredParams`: `object`[]; - `rp`: \{ - `id`: `string`; - `name`: `string`; - \}; - `status`: `"OK"`; - `timeout`: `number`; - `user`: \{ - `displayName`: `string`; - `id`: `string`; - `name`: `string`; - \}; - `webauthnGeneratedOptionsId`: `string`; -\} - \| `RegisterOptionsErrorResponse`\> - -##### removeCredential() - -```ts -removeCredential(input): Promise< - | { - status: "OK"; -} -| RemoveCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:351](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L351) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnCredentialId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveCredentialErrorResponse`\> - -##### removeGeneratedOptions() - -```ts -removeGeneratedOptions(input): Promise< - | { - status: "OK"; -} -| RemoveGeneratedOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:379](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L379) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveGeneratedOptionsErrorResponse`\> - -##### signIn() - -```ts -signIn(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} -| SignInErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:274](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L274) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](#authenticationpayload); `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](#authenticationpayload) | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignInErrorResponse`\> - -##### signInOptions() - -```ts -signInOptions(input): Promise< - | { - challenge: string; - createdAt: number; - expiresAt: number; - status: "OK"; - timeout: number; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} -| SignInOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:236](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L236) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `origin`: `string`; `relyingPartyId`: `string`; `relyingPartyName`: `string`; `tenantId`: `string`; `timeout`: `number` \| `undefined`; `userContext`: [`UserContext`](types.mdx#usercontext); `userPresence`: `boolean` \| `undefined`; `userVerification`: [`UserVerification`](#userverification) \| `undefined`; \} | -| `input.origin` | `string` | -| `input.relyingPartyId` | `string` | -| `input.relyingPartyName` | `string` | -| `input.tenantId` | `string` | -| `input.timeout` | `number` \| `undefined` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.userPresence` | `boolean` \| `undefined` | -| `input.userVerification` | [`UserVerification`](#userverification) \| `undefined` | - -###### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `expiresAt`: `number`; - `status`: `"OK"`; - `timeout`: `number`; - `userVerification`: [`UserVerification`](#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `SignInOptionsErrorResponse`\> - -##### signUp() - -```ts -signUp(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} -| SignUpErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:258](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L258) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](#registrationpayload); `session`: `SessionContainerInterface` \| `undefined`; `shouldTryLinkingWithSessionUser`: `boolean` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](#registrationpayload) | -| `input.session` | `SessionContainerInterface` \| `undefined` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignUpErrorResponse`\> - -##### updateUserEmail() - -```ts -updateUserEmail(input): Promise< - | { - status: "OK"; -} -| UpdateUserEmailErrorResponse>; -``` - -Defined in: [recipe/webauthn/types.ts:407](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L407) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: `string`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.email` | `string` | -| `input.recipeUserId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `UpdateUserEmailErrorResponse`\> - -##### verifyCredentials() - -```ts -verifyCredentials(input): Promise< - | VerifyCredentialsErrorResponse - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -}>; -``` - -Defined in: [recipe/webauthn/types.ts:283](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L283) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](#authenticationpayload); `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](#authenticationpayload) | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| `VerifyCredentialsErrorResponse` - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\}\> - -*** - -### RegistrationPayload - -```ts -type RegistrationPayload = CredentialPayloadBase & object; -``` - -Defined in: [recipe/webauthn/types.ts:707](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L707) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `response` | [`AuthenticatorAttestationResponseJSON`](#authenticatorattestationresponsejson) | [recipe/webauthn/types.ts:708](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L708) | - -*** - -### ResidentKey - -```ts -type ResidentKey = "required" | "preferred" | "discouraged"; -``` - -Defined in: [recipe/webauthn/types.ts:171](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L171) - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [recipe/webauthn/types.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L68) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emailDelivery?` | `EmailDeliveryTypeInput`\<[`TypeWebauthnEmailDeliveryInput`](#typewebauthnemaildeliveryinput)\> | [recipe/webauthn/types.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L69) | -| `getOrigin?` | [`TypeInputGetOrigin`](#typeinputgetorigin-1) | [recipe/webauthn/types.ts:73](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L73) | -| `getRelyingPartyId?` | [`TypeInputRelyingPartyId`](#typeinputrelyingpartyid) | [recipe/webauthn/types.ts:70](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L70) | -| `getRelyingPartyName?` | [`TypeInputRelyingPartyName`](#typeinputrelyingpartyname) | [recipe/webauthn/types.ts:71](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L71) | -| `override?` | `object` | [recipe/webauthn/types.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L74) | -| `override.apis?` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/webauthn/types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L79) | -| `override.functions?` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/webauthn/types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L75) | -| `validateEmailAddress?` | [`TypeInputValidateEmailAddress`](#typeinputvalidateemailaddress-1) | [recipe/webauthn/types.ts:72](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L72) | - -*** - -### TypeInputGetOrigin() - -```ts -type TypeInputGetOrigin = (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L91) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `request`: `BaseRequest`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.request` | `BaseRequest` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string`\> - -*** - -### TypeInputRelyingPartyId - -```ts -type TypeInputRelyingPartyId = string | (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:83](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L83) - -*** - -### TypeInputRelyingPartyName - -```ts -type TypeInputRelyingPartyName = string | (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L87) - -*** - -### TypeInputValidateEmailAddress() - -```ts -type TypeInputValidateEmailAddress = (email, tenantId, userContext) => Promise | string | undefined; -``` - -Defined in: [recipe/webauthn/types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L97) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `email` | `string` | -| `tenantId` | `string` | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string` \| `undefined`\> \| `string` \| `undefined` - -*** - -### TypeNormalisedInput - -```ts -type TypeNormalisedInput = object; -``` - -Defined in: [recipe/webauthn/types.ts:27](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L27) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `getEmailDeliveryConfig` | (`isInServerlessEnv`) => `EmailDeliveryTypeInputWithService`\<[`TypeWebauthnEmailDeliveryInput`](#typewebauthnemaildeliveryinput)\> | [recipe/webauthn/types.ts:31](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L31) | -| `getOrigin` | [`TypeNormalisedInputGetOrigin`](#typenormalisedinputgetorigin-1) | [recipe/webauthn/types.ts:30](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L30) | -| `getRelyingPartyId` | [`TypeNormalisedInputRelyingPartyId`](#typenormalisedinputrelyingpartyid) | [recipe/webauthn/types.ts:28](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L28) | -| `getRelyingPartyName` | [`TypeNormalisedInputRelyingPartyName`](#typenormalisedinputrelyingpartyname) | [recipe/webauthn/types.ts:29](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L29) | -| `override` | `object` | [recipe/webauthn/types.ts:35](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L35) | -| `override.apis` | (`originalImplementation`, `builder`) => [`APIInterface`](#apiinterface) | [recipe/webauthn/types.ts:40](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L40) | -| `override.functions` | (`originalImplementation`, `builder`) => [`RecipeInterface`](#recipeinterface) | [recipe/webauthn/types.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L36) | -| `validateEmailAddress` | [`TypeNormalisedInputValidateEmailAddress`](#typenormalisedinputvalidateemailaddress-1) | [recipe/webauthn/types.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L34) | - -*** - -### TypeNormalisedInputGetOrigin() - -```ts -type TypeNormalisedInputGetOrigin = (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:56](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L56) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `request`: `BaseRequest`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.request` | `BaseRequest` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string`\> - -*** - -### TypeNormalisedInputRelyingPartyId() - -```ts -type TypeNormalisedInputRelyingPartyId = (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:44](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L44) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `request`: `BaseRequest` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.request` | `BaseRequest` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string`\> - -*** - -### TypeNormalisedInputRelyingPartyName() - -```ts -type TypeNormalisedInputRelyingPartyName = (input) => Promise; -``` - -Defined in: [recipe/webauthn/types.ts:50](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L50) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `request`: `BaseRequest` \| `undefined`; `tenantId`: `string`; `userContext`: [`UserContext`](types.mdx#usercontext); \} | -| `input.request` | `BaseRequest` \| `undefined` | -| `input.tenantId` | `string` | -| `input.userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string`\> - -*** - -### TypeNormalisedInputValidateEmailAddress() - -```ts -type TypeNormalisedInputValidateEmailAddress = (email, tenantId, userContext) => Promise | string | undefined; -``` - -Defined in: [recipe/webauthn/types.ts:62](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L62) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `email` | `string` | -| `tenantId` | `string` | -| `userContext` | [`UserContext`](types.mdx#usercontext) | - -#### Returns - -`Promise`\<`string` \| `undefined`\> \| `string` \| `undefined` - -*** - -### TypeWebauthnEmailDeliveryInput - -```ts -type TypeWebauthnEmailDeliveryInput = TypeWebauthnRecoverAccountEmailDeliveryInput; -``` - -Defined in: [recipe/webauthn/types.ts:677](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L677) - -*** - -### TypeWebauthnRecoverAccountEmailDeliveryInput - -```ts -type TypeWebauthnRecoverAccountEmailDeliveryInput = object; -``` - -Defined in: [recipe/webauthn/types.ts:666](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L666) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `recoverAccountLink` | `string` | [recipe/webauthn/types.ts:673](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L673) | -| `tenantId` | `string` | [recipe/webauthn/types.ts:674](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L674) | -| `type` | `"RECOVER_ACCOUNT"` | [recipe/webauthn/types.ts:667](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L667) | -| `user` | `object` | [recipe/webauthn/types.ts:668](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L668) | -| `user.email` | `string` | [recipe/webauthn/types.ts:671](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L671) | -| `user.id` | `string` | [recipe/webauthn/types.ts:669](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L669) | -| `user.recipeUserId` | [`RecipeUserId`](index.mdx#recipeuserid-1) \| `undefined` | [recipe/webauthn/types.ts:670](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L670) | - -*** - -### UserVerification - -```ts -type UserVerification = "required" | "preferred" | "discouraged"; -``` - -Defined in: [recipe/webauthn/types.ts:172](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/types.ts#L172) diff --git a/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn.mdx b/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn.mdx deleted file mode 100644 index 211ddfc6ca..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/recipe-webauthn.mdx +++ /dev/null @@ -1,1496 +0,0 @@ ---- -title: supertokens-node/recipe/webauthn -page_title: WebAuthn -sidebar_position: 5 -description: References documentation for the supertokens-node/recipe/webauthn package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# WebAuthn - - -## Classes - -### default - -Defined in: [recipe/webauthn/index.ts:33](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L33) - -#### Constructors - -##### Constructor - -```ts -new default(): default; -``` - -###### Returns - -[`default`](#default) - -#### Properties - -| Property | Modifier | Type | Default value | Defined in | -| ------ | ------ | ------ | ------ | ------ | -| `Error` | `static` | *typeof* `default` | `SuperTokensError` | [recipe/webauthn/index.ts:36](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L36) | -| `init` | `static` | (`config?`) => [`RecipeListFunction`](types.mdx#recipelistfunction) | `Recipe.init` | [recipe/webauthn/index.ts:34](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L34) | - -#### Methods - -##### consumeRecoverAccountToken() - -```ts -static consumeRecoverAccountToken(input): Promise< - | { - email: string; - status: "OK"; - userId: string; -} -| ConsumeRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:191](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L191) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| `ConsumeRecoverAccountTokenErrorResponse`\> - -##### createRecoverAccountLink() - -```ts -static createRecoverAccountLink(__namedParameters): Promise< - | GenerateRecoverAccountTokenErrorResponse - | { - link: string; - status: string; -}>; -``` - -Defined in: [recipe/webauthn/index.ts:210](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L210) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `__namedParameters.email` | `string` | -| `__namedParameters.tenantId` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.userId` | `string` | - -###### Returns - -`Promise`\< - \| `GenerateRecoverAccountTokenErrorResponse` - \| \{ - `link`: `string`; - `status`: `string`; -\}\> - -##### generateRecoverAccountToken() - -```ts -static generateRecoverAccountToken(input): Promise< - | { - status: "OK"; - token: string; -} -| GenerateRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:150](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L150) - -We do not make email optional here because we want to -allow passing in primaryUserId. If we make email optional, -and if the user provides a primaryUserId, then it may result in two problems: - - there is no recipeUserId = input primaryUserId, in this case, - this function will throw an error - - There is a recipe userId = input primaryUserId, but that recipe has no email, - or has wrong email compared to what the user wanted to generate a reset token for. - -And we want to allow primaryUserId being passed in. - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| `GenerateRecoverAccountTokenErrorResponse`\> - -##### getCredential() - -```ts -static getCredential(input): Promise< - | { - createdAt: number; - recipeUserId: RecipeUserId; - relyingPartyId: string; - status: "OK"; - webauthnCredentialId: string; -} -| GetCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:322](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L322) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnCredentialId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `createdAt`: `number`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `relyingPartyId`: `string`; - `status`: `"OK"`; - `webauthnCredentialId`: `string`; -\} - \| `GetCredentialErrorResponse`\> - -##### getGeneratedOptions() - -```ts -static getGeneratedOptions(input): Promise< - | { - challenge: string; - createdAt: number; - email?: string; - expiresAt: number; - origin: string; - relyingPartyId: string; - relyingPartyName: string; - status: "OK"; - timeout: number; - userPresence: boolean; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} -| GetGeneratedOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:83](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L83) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `email?`: `string`; - `expiresAt`: `number`; - `origin`: `string`; - `relyingPartyId`: `string`; - `relyingPartyName`: `string`; - `status`: `"OK"`; - `timeout`: `number`; - `userPresence`: `boolean`; - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `GetGeneratedOptionsErrorResponse`\> - -##### getUserFromRecoverAccountToken() - -```ts -static getUserFromRecoverAccountToken(input): Promise< - | { - recipeUserId?: RecipeUserId; - status: "OK"; - user: User; -} -| GetUserFromRecoverAccountTokenErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:289](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L289) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId?`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `GetUserFromRecoverAccountTokenErrorResponse`\> - -##### listCredentials() - -```ts -static listCredentials(input): Promise<{ - credentials: object[]; - status: "OK"; -}>; -``` - -Defined in: [recipe/webauthn/index.ts:333](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L333) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\<\{ - `credentials`: `object`[]; - `status`: `"OK"`; -\}\> - -##### recoverAccount() - -```ts -static recoverAccount(__namedParameters): Promise< - | ConsumeRecoverAccountTokenErrorResponse - | { - status: "OK"; -} -| RegisterCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:162](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L162) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `tenantId?`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `__namedParameters.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `__namedParameters.tenantId?` | `string` | -| `__namedParameters.token` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| `ConsumeRecoverAccountTokenErrorResponse` - \| \{ - `status`: `"OK"`; -\} - \| `RegisterCredentialErrorResponse`\> - -##### registerCredential() - -```ts -static registerCredential(input): Promise< - | { - status: "OK"; -} -| RegisterCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L198) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RegisterCredentialErrorResponse`\> - -##### registerOptions() - -```ts -static registerOptions(input): Promise< - | { - attestation: Attestation; - authenticatorSelection: { - requireResidentKey: boolean; - residentKey: ResidentKey; - userVerification: UserVerification; - }; - challenge: string; - createdAt: number; - excludeCredentials: object[]; - expiresAt: number; - pubKeyCredParams: object[]; - rp: { - id: string; - name: string; - }; - status: "OK"; - timeout: number; - user: { - displayName: string; - id: string; - name: string; - }; - webauthnGeneratedOptionsId: string; -} -| RegisterOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:38](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L38) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | `object` & \| \{ `recoverAccountToken`: `string`; \} \| \{ `displayName`: `string`; `email`: `string`; \} | - -###### Returns - -`Promise`\< - \| \{ - `attestation`: [`Attestation`](recipe-webauthn-types.mdx#attestation); - `authenticatorSelection`: \{ - `requireResidentKey`: `boolean`; - `residentKey`: [`ResidentKey`](recipe-webauthn-types.mdx#residentkey); - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - \}; - `challenge`: `string`; - `createdAt`: `number`; - `excludeCredentials`: `object`[]; - `expiresAt`: `number`; - `pubKeyCredParams`: `object`[]; - `rp`: \{ - `id`: `string`; - `name`: `string`; - \}; - `status`: `"OK"`; - `timeout`: `number`; - `user`: \{ - `displayName`: `string`; - `id`: `string`; - `name`: `string`; - \}; - `webauthnGeneratedOptionsId`: `string`; -\} - \| `RegisterOptionsErrorResponse`\> - -##### removeCredential() - -```ts -static removeCredential(input): Promise< - | { - status: "OK"; -} -| RemoveCredentialErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:311](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L311) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnCredentialId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveCredentialErrorResponse`\> - -##### removeGeneratedOptions() - -```ts -static removeGeneratedOptions(input): Promise< - | { - status: "OK"; -} -| RemoveGeneratedOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:300](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L300) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveGeneratedOptionsErrorResponse`\> - -##### sendEmail() - -```ts -static sendEmail(input): Promise; -``` - -Defined in: [recipe/webauthn/index.ts:281](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L281) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeWebauthnRecoverAccountEmailDeliveryInput`](recipe-webauthn-types.mdx#typewebauthnrecoveraccountemaildeliveryinput) & `object` | - -###### Returns - -`Promise`\<`void`\> - -##### sendRecoverAccountEmail() - -```ts -static sendRecoverAccountEmail(__namedParameters): Promise< - | { - link: string; - status: string; -} - | { - status: string; -}>; -``` - -Defined in: [recipe/webauthn/index.ts:240](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L240) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `__namedParameters.email` | `string` | -| `__namedParameters.tenantId` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.userId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `string`; -\} - \| \{ - `status`: `string`; -\}\> - -##### signIn() - -```ts -static signIn(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} -| SignInErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:108](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L108) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload); `session`: `SessionContainerInterface`; `shouldTryLinkingWithSessionUser`: `boolean`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload) | -| `input.session` | `SessionContainerInterface` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignInErrorResponse`\> - -##### signInOptions() - -```ts -static signInOptions(input): Promise< - | { - challenge: string; - createdAt: number; - expiresAt: number; - status: "OK"; - timeout: number; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} -| SignInOptionsErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:67](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L67) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `origin`: `string`; `relyingPartyId`: `string`; `relyingPartyName`: `string`; `tenantId`: `string`; `timeout`: `number`; `userContext?`: `Record`\<`string`, `any`\>; `userPresence`: `boolean`; `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); \} | -| `input.origin` | `string` | -| `input.relyingPartyId` | `string` | -| `input.relyingPartyName` | `string` | -| `input.tenantId` | `string` | -| `input.timeout` | `number` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userPresence` | `boolean` | -| `input.userVerification` | [`UserVerification`](recipe-webauthn-types.mdx#userverification) | - -###### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `expiresAt`: `number`; - `status`: `"OK"`; - `timeout`: `number`; - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `SignInOptionsErrorResponse`\> - -##### signUp() - -```ts -static signUp(input): Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} -| SignUpErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L94) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `session`: `SessionContainerInterface`; `shouldTryLinkingWithSessionUser`: `boolean`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `input.session` | `SessionContainerInterface` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignUpErrorResponse`\> - -##### updateUserEmail() - -```ts -static updateUserEmail(input): Promise< - | { - status: "OK"; -} -| UpdateUserEmailErrorResponse>; -``` - -Defined in: [recipe/webauthn/index.ts:340](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L340) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email` | `string` | -| `input.recipeUserId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -###### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `UpdateUserEmailErrorResponse`\> - -##### verifyCredentials() - -```ts -static verifyCredentials(input): Promise<{ - status: | "OK" - | "UNKNOWN_USER_ID_ERROR" - | "INVALID_CREDENTIALS_ERROR" - | "INVALID_OPTIONS_ERROR" - | "INVALID_AUTHENTICATOR_ERROR" - | "CREDENTIAL_NOT_FOUND_ERROR" - | "OPTIONS_NOT_FOUND_ERROR"; -}>; -``` - -Defined in: [recipe/webauthn/index.ts:122](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L122) - -###### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload); `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload) | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -###### Returns - -`Promise`\<\{ - `status`: \| `"OK"` - \| `"UNKNOWN_USER_ID_ERROR"` - \| `"INVALID_CREDENTIALS_ERROR"` - \| `"INVALID_OPTIONS_ERROR"` - \| `"INVALID_AUTHENTICATOR_ERROR"` - \| `"CREDENTIAL_NOT_FOUND_ERROR"` - \| `"OPTIONS_NOT_FOUND_ERROR"`; -\}\> - -## Variables - -### consumeRecoverAccountToken() - -```ts -consumeRecoverAccountToken: (input) => Promise< - | { - email: string; - status: "OK"; - userId: string; -} - | ConsumeRecoverAccountTokenErrorResponse> = Wrapper.consumeRecoverAccountToken; -``` - -Defined in: [recipe/webauthn/index.ts:371](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L371) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `email`: `string`; - `status`: `"OK"`; - `userId`: `string`; -\} - \| `ConsumeRecoverAccountTokenErrorResponse`\> - -*** - -### createRecoverAccountLink() - -```ts -createRecoverAccountLink: (__namedParameters) => Promise< - | GenerateRecoverAccountTokenErrorResponse - | { - link: string; - status: string; -}> = Wrapper.createRecoverAccountLink; -``` - -Defined in: [recipe/webauthn/index.ts:375](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L375) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `__namedParameters.email` | `string` | -| `__namedParameters.tenantId` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.userId` | `string` | - -#### Returns - -`Promise`\< - \| `GenerateRecoverAccountTokenErrorResponse` - \| \{ - `link`: `string`; - `status`: `string`; -\}\> - -*** - -### Error - -```ts -Error: typeof default = Wrapper.Error; -``` - -Defined in: [recipe/webauthn/index.ts:355](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L355) - -*** - -### generateRecoverAccountToken() - -```ts -generateRecoverAccountToken: (input) => Promise< - | { - status: "OK"; - token: string; -} - | GenerateRecoverAccountTokenErrorResponse> = Wrapper.generateRecoverAccountToken; -``` - -Defined in: [recipe/webauthn/index.ts:367](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L367) - -We do not make email optional here because we want to -allow passing in primaryUserId. If we make email optional, -and if the user provides a primaryUserId, then it may result in two problems: - - there is no recipeUserId = input primaryUserId, in this case, - this function will throw an error - - There is a recipe userId = input primaryUserId, but that recipe has no email, - or has wrong email compared to what the user wanted to generate a reset token for. - -And we want to allow primaryUserId being passed in. - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `input.email` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; - `token`: `string`; -\} - \| `GenerateRecoverAccountTokenErrorResponse`\> - -*** - -### getCredential() - -```ts -getCredential: (input) => Promise< - | { - createdAt: number; - recipeUserId: RecipeUserId; - relyingPartyId: string; - status: "OK"; - webauthnCredentialId: string; -} - | GetCredentialErrorResponse> = Wrapper.getCredential; -``` - -Defined in: [recipe/webauthn/index.ts:389](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L389) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnCredentialId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `createdAt`: `number`; - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `relyingPartyId`: `string`; - `status`: `"OK"`; - `webauthnCredentialId`: `string`; -\} - \| `GetCredentialErrorResponse`\> - -*** - -### getGeneratedOptions() - -```ts -getGeneratedOptions: (input) => Promise< - | { - challenge: string; - createdAt: number; - email?: string; - expiresAt: number; - origin: string; - relyingPartyId: string; - relyingPartyName: string; - status: "OK"; - timeout: number; - userPresence: boolean; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} - | GetGeneratedOptionsErrorResponse> = Wrapper.getGeneratedOptions; -``` - -Defined in: [recipe/webauthn/index.ts:381](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L381) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `email?`: `string`; - `expiresAt`: `number`; - `origin`: `string`; - `relyingPartyId`: `string`; - `relyingPartyName`: `string`; - `status`: `"OK"`; - `timeout`: `number`; - `userPresence`: `boolean`; - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `GetGeneratedOptionsErrorResponse`\> - -*** - -### getUserFromRecoverAccountToken() - -```ts -getUserFromRecoverAccountToken: (input) => Promise< - | { - recipeUserId?: RecipeUserId; - status: "OK"; - user: User; -} - | GetUserFromRecoverAccountTokenErrorResponse> = Wrapper.getUserFromRecoverAccountToken; -``` - -Defined in: [recipe/webauthn/index.ts:383](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L383) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.tenantId` | `string` | -| `input.token` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `recipeUserId?`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `GetUserFromRecoverAccountTokenErrorResponse`\> - -*** - -### init() - -```ts -init: (config?) => RecipeListFunction = Wrapper.init; -``` - -Defined in: [recipe/webauthn/index.ts:353](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L353) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `config?` | [`TypeInput`](recipe-webauthn-types.mdx#typeinput) | - -#### Returns - -[`RecipeListFunction`](types.mdx#recipelistfunction) - -*** - -### listCredentials() - -```ts -listCredentials: (input) => Promise<{ - credentials: object[]; - status: "OK"; -}> = Wrapper.listCredentials; -``` - -Defined in: [recipe/webauthn/index.ts:391](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L391) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\<\{ - `credentials`: `object`[]; - `status`: `"OK"`; -\}\> - -*** - -### recoverAccount() - -```ts -recoverAccount: (__namedParameters) => Promise< - | ConsumeRecoverAccountTokenErrorResponse - | { - status: "OK"; -} - | RegisterCredentialErrorResponse> = Wrapper.recoverAccount; -``` - -Defined in: [recipe/webauthn/index.ts:369](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L369) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `tenantId?`: `string`; `token`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `__namedParameters.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `__namedParameters.tenantId?` | `string` | -| `__namedParameters.token` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| `ConsumeRecoverAccountTokenErrorResponse` - \| \{ - `status`: `"OK"`; -\} - \| `RegisterCredentialErrorResponse`\> - -*** - -### registerCredential() - -```ts -registerCredential: (input) => Promise< - | { - status: "OK"; -} - | RegisterCredentialErrorResponse> = Wrapper.registerCredential; -``` - -Defined in: [recipe/webauthn/index.ts:373](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L373) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RegisterCredentialErrorResponse`\> - -*** - -### registerOptions() - -```ts -registerOptions: (input) => Promise< - | { - attestation: Attestation; - authenticatorSelection: { - requireResidentKey: boolean; - residentKey: ResidentKey; - userVerification: UserVerification; - }; - challenge: string; - createdAt: number; - excludeCredentials: object[]; - expiresAt: number; - pubKeyCredParams: object[]; - rp: { - id: string; - name: string; - }; - status: "OK"; - timeout: number; - user: { - displayName: string; - id: string; - name: string; - }; - webauthnGeneratedOptionsId: string; -} - | RegisterOptionsErrorResponse> = Wrapper.registerOptions; -``` - -Defined in: [recipe/webauthn/index.ts:357](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L357) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | `object` & \| \{ `recoverAccountToken`: `string`; \} \| \{ `displayName`: `string`; `email`: `string`; \} | - -#### Returns - -`Promise`\< - \| \{ - `attestation`: [`Attestation`](recipe-webauthn-types.mdx#attestation); - `authenticatorSelection`: \{ - `requireResidentKey`: `boolean`; - `residentKey`: [`ResidentKey`](recipe-webauthn-types.mdx#residentkey); - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - \}; - `challenge`: `string`; - `createdAt`: `number`; - `excludeCredentials`: `object`[]; - `expiresAt`: `number`; - `pubKeyCredParams`: `object`[]; - `rp`: \{ - `id`: `string`; - `name`: `string`; - \}; - `status`: `"OK"`; - `timeout`: `number`; - `user`: \{ - `displayName`: `string`; - `id`: `string`; - `name`: `string`; - \}; - `webauthnGeneratedOptionsId`: `string`; -\} - \| `RegisterOptionsErrorResponse`\> - -*** - -### removeCredential() - -```ts -removeCredential: (input) => Promise< - | { - status: "OK"; -} - | RemoveCredentialErrorResponse> = Wrapper.removeCredential; -``` - -Defined in: [recipe/webauthn/index.ts:387](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L387) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `recipeUserId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnCredentialId`: `string`; \} | -| `input.recipeUserId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnCredentialId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveCredentialErrorResponse`\> - -*** - -### removeGeneratedOptions() - -```ts -removeGeneratedOptions: (input) => Promise< - | { - status: "OK"; -} - | RemoveGeneratedOptionsErrorResponse> = Wrapper.removeGeneratedOptions; -``` - -Defined in: [recipe/webauthn/index.ts:385](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L385) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `RemoveGeneratedOptionsErrorResponse`\> - -*** - -### sendEmail() - -```ts -sendEmail: (input) => Promise = Wrapper.sendEmail; -``` - -Defined in: [recipe/webauthn/index.ts:379](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L379) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | [`TypeWebauthnRecoverAccountEmailDeliveryInput`](recipe-webauthn-types.mdx#typewebauthnrecoveraccountemaildeliveryinput) & `object` | - -#### Returns - -`Promise`\<`void`\> - -*** - -### sendRecoverAccountEmail() - -```ts -sendRecoverAccountEmail: (__namedParameters) => Promise< - | { - link: string; - status: string; -} - | { - status: string; -}> = Wrapper.sendRecoverAccountEmail; -``` - -Defined in: [recipe/webauthn/index.ts:377](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L377) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `__namedParameters` | \{ `email`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `userId`: `string`; \} | -| `__namedParameters.email` | `string` | -| `__namedParameters.tenantId` | `string` | -| `__namedParameters.userContext?` | `Record`\<`string`, `any`\> | -| `__namedParameters.userId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `link`: `string`; - `status`: `string`; -\} - \| \{ - `status`: `string`; -\}\> - -*** - -### signIn() - -```ts -signIn: (input) => Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | SignInErrorResponse> = Wrapper.signIn; -``` - -Defined in: [recipe/webauthn/index.ts:361](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L361) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload); `session`: `SessionContainerInterface`; `shouldTryLinkingWithSessionUser`: `boolean`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload) | -| `input.session` | `SessionContainerInterface` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignInErrorResponse`\> - -*** - -### signInOptions() - -```ts -signInOptions: (input) => Promise< - | { - challenge: string; - createdAt: number; - expiresAt: number; - status: "OK"; - timeout: number; - userVerification: UserVerification; - webauthnGeneratedOptionsId: string; -} - | SignInOptionsErrorResponse> = Wrapper.signInOptions; -``` - -Defined in: [recipe/webauthn/index.ts:359](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L359) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `origin`: `string`; `relyingPartyId`: `string`; `relyingPartyName`: `string`; `tenantId`: `string`; `timeout`: `number`; `userContext?`: `Record`\<`string`, `any`\>; `userPresence`: `boolean`; `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); \} | -| `input.origin` | `string` | -| `input.relyingPartyId` | `string` | -| `input.relyingPartyName` | `string` | -| `input.tenantId` | `string` | -| `input.timeout` | `number` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.userPresence` | `boolean` | -| `input.userVerification` | [`UserVerification`](recipe-webauthn-types.mdx#userverification) | - -#### Returns - -`Promise`\< - \| \{ - `challenge`: `string`; - `createdAt`: `number`; - `expiresAt`: `number`; - `status`: `"OK"`; - `timeout`: `number`; - `userVerification`: [`UserVerification`](recipe-webauthn-types.mdx#userverification); - `webauthnGeneratedOptionsId`: `string`; -\} - \| `SignInOptionsErrorResponse`\> - -*** - -### signUp() - -```ts -signUp: (input) => Promise< - | { - recipeUserId: RecipeUserId; - status: "OK"; - user: User; -} - | SignUpErrorResponse> = Wrapper.signUp; -``` - -Defined in: [recipe/webauthn/index.ts:363](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L363) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload); `session`: `SessionContainerInterface`; `shouldTryLinkingWithSessionUser`: `boolean`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`RegistrationPayload`](recipe-webauthn-types.mdx#registrationpayload) | -| `input.session` | `SessionContainerInterface` | -| `input.shouldTryLinkingWithSessionUser` | `boolean` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\< - \| \{ - `recipeUserId`: [`RecipeUserId`](index.mdx#recipeuserid-1); - `status`: `"OK"`; - `user`: [`User`](types.mdx#user); -\} - \| `SignUpErrorResponse`\> - -*** - -### updateUserEmail() - -```ts -updateUserEmail: (input) => Promise< - | { - status: "OK"; -} - | UpdateUserEmailErrorResponse> = Wrapper.updateUserEmail; -``` - -Defined in: [recipe/webauthn/index.ts:393](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L393) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `email`: `string`; `recipeUserId`: `string`; `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; \} | -| `input.email` | `string` | -| `input.recipeUserId` | `string` | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | - -#### Returns - -`Promise`\< - \| \{ - `status`: `"OK"`; -\} - \| `UpdateUserEmailErrorResponse`\> - -*** - -### verifyCredentials() - -```ts -verifyCredentials: (input) => Promise<{ - status: | "OK" - | "UNKNOWN_USER_ID_ERROR" - | "INVALID_CREDENTIALS_ERROR" - | "INVALID_OPTIONS_ERROR" - | "INVALID_AUTHENTICATOR_ERROR" - | "CREDENTIAL_NOT_FOUND_ERROR" - | "OPTIONS_NOT_FOUND_ERROR"; -}> = Wrapper.verifyCredentials; -``` - -Defined in: [recipe/webauthn/index.ts:365](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/recipe/webauthn/index.ts#L365) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `input` | \{ `credential`: [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload); `tenantId`: `string`; `userContext?`: `Record`\<`string`, `any`\>; `webauthnGeneratedOptionsId`: `string`; \} | -| `input.credential` | [`AuthenticationPayload`](recipe-webauthn-types.mdx#authenticationpayload) | -| `input.tenantId` | `string` | -| `input.userContext?` | `Record`\<`string`, `any`\> | -| `input.webauthnGeneratedOptionsId` | `string` | - -#### Returns - -`Promise`\<\{ - `status`: \| `"OK"` - \| `"UNKNOWN_USER_ID_ERROR"` - \| `"INVALID_CREDENTIALS_ERROR"` - \| `"INVALID_OPTIONS_ERROR"` - \| `"INVALID_AUTHENTICATOR_ERROR"` - \| `"CREDENTIAL_NOT_FOUND_ERROR"` - \| `"OPTIONS_NOT_FOUND_ERROR"`; -\}\> - -## References - -### APIInterface - -Re-exports [APIInterface](recipe-webauthn-types.mdx#apiinterface) - -*** - -### APIOptions - -Re-exports [APIOptions](recipe-webauthn-types.mdx#apioptions) - -*** - -### RecipeInterface - -Re-exports [RecipeInterface](recipe-webauthn-types.mdx#recipeinterface) diff --git a/docs/references/backend-sdks/supertokens-nodejs/types.mdx b/docs/references/backend-sdks/supertokens-nodejs/types.mdx deleted file mode 100644 index 1f551b26e2..0000000000 --- a/docs/references/backend-sdks/supertokens-nodejs/types.mdx +++ /dev/null @@ -1,486 +0,0 @@ ---- -title: supertokens-node/types -page_title: SuperTokens Node Types -sidebar_position: 1 -description: References documentation for the supertokens-node/types package -hide_title: true -page_type: sdk-reference -package_name: supertokens-node -category: references ---- - -# SuperTokens Node Types - - -## Interfaces - -### HttpRequest - -Defined in: [types.ts:197](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L197) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `body?` | `any` | [types.ts:202](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L202) | -| `headers` | `object` | [types.ts:200](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L200) | -| `method` | [`HTTPMethod`](#httpmethod) | [types.ts:199](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L199) | -| `params?` | `Record`\<`string`, `boolean` \| `number` \| `string` \| `undefined`\> | [types.ts:201](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L201) | -| `url` | `string` | [types.ts:198](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L198) | - -*** - -### JSONObject - -Defined in: [types.ts:223](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L223) - -#### Indexable - -```ts -[ind: string]: JSONValue -``` - -## Type Aliases - -### AllRecipeConfigs - -```ts -type AllRecipeConfigs = object; -``` - -Defined in: [types.ts:90](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L90) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `accountlinking` | [`TypeInput`](recipe-accountlinking-types.mdx#typeinput) & `object` | [types.ts:91](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L91) | -| `dashboard` | [`TypeInput`](recipe-dashboard-types.mdx#typeinput) | [types.ts:92](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L92) | -| `emailpassword` | [`TypeInput`](recipe-emailpassword-types.mdx#typeinput) | [types.ts:93](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L93) | -| `emailverification` | [`TypeInput`](recipe-emailverification-types.mdx#typeinput) | [types.ts:94](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L94) | -| `jwt` | [`TypeInput`](recipe-jwt-types.mdx#typeinput) | [types.ts:95](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L95) | -| `multifactorauth` | [`TypeInput`](recipe-multifactorauth-types.mdx#typeinput) | [types.ts:96](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L96) | -| `multitenancy` | [`TypeInput`](recipe-multitenancy-types.mdx#typeinput) | [types.ts:97](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L97) | -| `oauth2provider` | [`TypeInput`](recipe-oauth2provider-types.mdx#typeinput) | [types.ts:98](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L98) | -| `openid` | [`TypeInput`](recipe-openid-types.mdx#typeinput) | [types.ts:99](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L99) | -| `passwordless` | [`TypeInput`](recipe-passwordless-types.mdx#typeinput) | [types.ts:100](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L100) | -| `session` | `SessionTypeInput` | [types.ts:101](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L101) | -| `thirdparty` | [`TypeInput`](recipe-thirdparty-types.mdx#typeinput) | [types.ts:102](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L102) | -| `totp` | [`TypeInput`](recipe-totp-types.mdx#typeinput) | [types.ts:103](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L103) | -| `usermetadata` | [`TypeInput`](recipe-usermetadata-types.mdx#typeinput) | [types.ts:104](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L104) | -| `userroles` | [`TypeInput`](recipe-userroles-types.mdx#typeinput) | [types.ts:105](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L105) | -| `webauthn` | [`TypeInput`](recipe-webauthn-types.mdx#typeinput) | [types.ts:106](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L106) | - -*** - -### APIHandled - -```ts -type APIHandled = object; -``` - -Defined in: [types.ts:211](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L211) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `disabled` | `boolean` | [types.ts:215](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L215) | -| `id` | `string` | [types.ts:214](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L214) | -| `method` | [`HTTPMethod`](#httpmethod) | [types.ts:213](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L213) | -| `pathWithoutApiBasePath` | `NormalisedURLPath` | [types.ts:212](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L212) | - -*** - -### AppInfo - -```ts -type AppInfo = object; -``` - -Defined in: [types.ts:62](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L62) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `apiBasePath?` | `string` | [types.ts:68](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L68) | -| `apiDomain` | `string` | [types.ts:67](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L67) | -| `apiGatewayPath?` | `string` | [types.ts:69](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L69) | -| `appName` | `string` | [types.ts:63](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L63) | -| `origin?` | `string` \| (`input`) => `string` | [types.ts:65](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L65) | -| `websiteBasePath?` | `string` | [types.ts:66](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L66) | -| `websiteDomain?` | `string` | [types.ts:64](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L64) | - -*** - -### Entries\ - -```ts -type Entries = { [K in keyof T]-?: [K, T[K]] }[keyof T][]; -``` - -Defined in: [types.ts:53](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L53) - -#### Type Parameters - -| Type Parameter | -| ------ | -| `T` | - -*** - -### GeneralErrorResponse - -```ts -type GeneralErrorResponse = object; -``` - -Defined in: [types.ts:226](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L226) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `message` | `string` | [types.ts:228](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L228) | -| `status` | `"GENERAL_ERROR"` | [types.ts:227](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L227) | - -*** - -### HTTPMethod - -```ts -type HTTPMethod = "post" | "get" | "delete" | "put" | "patch" | "options" | "trace"; -``` - -Defined in: [types.ts:218](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L218) - -*** - -### JSONArray - -```ts -type JSONArray = JSONValue[]; -``` - -Defined in: [types.ts:221](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L221) - -*** - -### JSONPrimitive - -```ts -type JSONPrimitive = string | number | boolean | null; -``` - -Defined in: [types.ts:220](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L220) - -*** - -### JSONValue - -```ts -type JSONValue = - | JSONPrimitive - | JSONObject - | JSONArray - | undefined; -``` - -Defined in: [types.ts:222](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L222) - -*** - -### NetworkInterceptor() - -```ts -type NetworkInterceptor = (request, userContext) => HttpRequest; -``` - -Defined in: [types.ts:195](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L195) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `request` | [`HttpRequest`](#httprequest) | -| `userContext` | [`UserContext`](#usercontext) | - -#### Returns - -[`HttpRequest`](#httprequest) - -*** - -### NonNullableProperties\ - -```ts -type NonNullableProperties = { [P in keyof T]: NonNullable }; -``` - -Defined in: [types.ts:49](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L49) - -#### Type Parameters - -| Type Parameter | -| ------ | -| `T` | - -*** - -### NonPublicConfigPropertiesType - -```ts -type NonPublicConfigPropertiesType = typeof nonPublicConfigProperties[number]; -``` - -Defined in: [types.ts:163](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L163) - -*** - -### NormalisedAppinfo - -```ts -type NormalisedAppinfo = object; -``` - -Defined in: [types.ts:72](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L72) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `apiBasePath` | `NormalisedURLPath` | [types.ts:78](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L78) | -| `apiDomain` | `NormalisedURLDomain` | [types.ts:75](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L75) | -| `apiGatewayPath` | `NormalisedURLPath` | [types.ts:79](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L79) | -| `appName` | `string` | [types.ts:73](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L73) | -| `getOrigin` | (`input`) => `NormalisedURLDomain` | [types.ts:74](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L74) | -| `getTopLevelWebsiteDomain` | (`input`) => `string` | [types.ts:77](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L77) | -| `topLevelAPIDomain` | `string` | [types.ts:76](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L76) | -| `websiteBasePath` | `NormalisedURLPath` | [types.ts:80](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L80) | - -*** - -### PluginRouteHandler - -```ts -type PluginRouteHandler = object; -``` - -Defined in: [types.ts:115](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L115) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `handler` | (`req`, `res`, `session`, `userContext`) => `Promise`\< \| \{ `body`: [`JSONObject`](#jsonobject); `status`: `number`; \} \| `null`\> | [types.ts:119](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L119) | -| `method` | [`HTTPMethod`](#httpmethod) | [types.ts:116](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L116) | -| `path` | `string` | [types.ts:117](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L117) | -| `verifySessionOptions?` | `VerifySessionOptions` | [types.ts:118](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L118) | - -*** - -### RecipeListFunction() - -```ts -type RecipeListFunction = (appInfo, isInServerlessEnv, overrideMaps) => RecipeModule; -``` - -Defined in: [types.ts:205](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L205) - -#### Parameters - -| Parameter | Type | -| ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](#normalisedappinfo) | -| `isInServerlessEnv` | `boolean` | -| `overrideMaps` | `NonNullable`\<[`SuperTokensPlugin`](#supertokensplugin)\[`"overrideMap"`\]\>[] | - -#### Returns - -`RecipeModule` - -*** - -### RecipePluginOverride\ - -```ts -type RecipePluginOverride = object; -``` - -Defined in: [types.ts:109](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L109) - -#### Type Parameters - -| Type Parameter | -| ------ | -| `T` *extends* keyof [`AllRecipeConfigs`](#allrecipeconfigs) | - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `apis?` | `NonNullable`\<[`AllRecipeConfigs`](#allrecipeconfigs)\[`T`\]\[`"override"`\]\>\[`"apis"`\] | [types.ts:111](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L111) | -| `config?` | (`config`) => [`AllRecipeConfigs`](#allrecipeconfigs)\[`T`\] | [types.ts:112](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L112) | -| `functions?` | `NonNullable`\<[`AllRecipeConfigs`](#allrecipeconfigs)\[`T`\]\[`"override"`\]\>\[`"functions"`\] | [types.ts:110](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L110) | - -*** - -### SuperTokensConfigWithNormalisedAppInfo - -```ts -type SuperTokensConfigWithNormalisedAppInfo = Omit & object; -``` - -Defined in: [types.ts:165](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L165) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](#normalisedappinfo) | [types.ts:166](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L166) | - -*** - -### SuperTokensInfo - -```ts -type SuperTokensInfo = object; -``` - -Defined in: [types.ts:83](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L83) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `apiKey?` | `string` | [types.ts:85](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L85) | -| `connectionURI` | `string` | [types.ts:84](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L84) | -| `disableCoreCallCache?` | `boolean` | [types.ts:87](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L87) | -| `networkInterceptor?` | [`NetworkInterceptor`](#networkinterceptor) | [types.ts:86](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L86) | - -*** - -### SuperTokensPlugin - -```ts -type SuperTokensPlugin = object; -``` - -Defined in: [types.ts:130](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L130) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `compatibleSDKVersions?` | `string` \| `string`[] | [types.ts:133](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L133) | -| `config?` | (`config`) => \| `Omit`\<[`SuperTokensPublicConfig`](#supertokenspublicconfig), `"appInfo"`\> \| `undefined` | [types.ts:152](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L152) | -| `dependencies?` | (`config`, `pluginsAbove`, `sdkVersion`) => \| \{ `pluginsToAdd?`: [`SuperTokensPlugin`](#supertokensplugin)[]; `status`: `"OK"`; \} \| \{ `message`: `string`; `status`: `"ERROR"`; \} | [types.ts:135](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L135) | -| `exports?` | `Record`\<`string`, `any`\> | [types.ts:153](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L153) | -| `id` | `string` | [types.ts:131](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L131) | -| `init?` | (`config`, `allPlugins`, `sdkVersion`) => `void` | [types.ts:134](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L134) | -| `overrideMap?` | \{ \[recipeId in keyof AllRecipeConfigs\]?: RecipePluginOverride\ & \{ recipeInitRequired?: boolean \| ((sdkVersion: string) =\> boolean) \} \} | [types.ts:140](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L140) | -| `routeHandlers?` | \| (`config`, `allPlugins`, `sdkVersion`) => \| \{ `routeHandlers`: [`PluginRouteHandler`](#pluginroutehandler)[]; `status`: `"OK"`; \} \| \{ `message`: `string`; `status`: `"ERROR"`; \} \| [`PluginRouteHandler`](#pluginroutehandler)[] | [types.ts:145](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L145) | -| `version?` | `string` | [types.ts:132](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L132) | - -*** - -### SuperTokensPublicConfig - -```ts -type SuperTokensPublicConfig = Omit, "appInfo"> & object; -``` - -Defined in: [types.ts:169](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L169) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `appInfo` | [`NormalisedAppinfo`](#normalisedappinfo) | [types.ts:170](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L170) | - -*** - -### SuperTokensPublicPlugin - -```ts -type SuperTokensPublicPlugin = Pick & object; -``` - -Defined in: [types.ts:156](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L156) - -#### Type Declaration - -| Name | Type | Defined in | -| ------ | ------ | ------ | -| `initialized` | `boolean` | [types.ts:159](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L159) | - -*** - -### TypeInput - -```ts -type TypeInput = object; -``` - -Defined in: [types.ts:173](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L173) - -#### Properties - -| Property | Type | Description | Defined in | -| ------ | ------ | ------ | ------ | -| `appInfo` | [`AppInfo`](#appinfo) | - | [types.ts:176](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L176) | -| `debug?` | `boolean` | - | [types.ts:180](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L180) | -| `experimental?` | `object` | Our experimental features are not yet stable and are subject to change. In practical terms, this means that their interface is subject to change without a major version update. They are also not tested as much as our "normal" features. If you want to use these features, or if you have any feedback please let us know at: https://supertokens.com/discord | [types.ts:190](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L190) | -| `experimental.plugins?` | [`SuperTokensPlugin`](#supertokensplugin)[] | - | [types.ts:191](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L191) | -| `framework?` | `TypeFramework` | - | [types.ts:175](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L175) | -| `isInServerlessEnv?` | `boolean` | - | [types.ts:179](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L179) | -| `recipeList` | [`RecipeListFunction`](#recipelistfunction)[] | - | [types.ts:177](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L177) | -| `supertokens?` | [`SuperTokensInfo`](#supertokensinfo) | - | [types.ts:174](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L174) | -| `telemetry?` | `boolean` | - | [types.ts:178](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L178) | - -*** - -### User - -```ts -type User = object; -``` - -Defined in: [types.ts:231](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L231) - -#### Properties - -| Property | Type | Defined in | -| ------ | ------ | ------ | -| `emails` | `string`[] | [types.ts:236](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L236) | -| `id` | `string` | [types.ts:232](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L232) | -| `isPrimaryUser` | `boolean` | [types.ts:234](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L234) | -| `loginMethods` | [`RecipeLevelUser`](recipe-accountlinking-types.mdx#recipeleveluser) & `object`[] | [types.ts:245](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L245) | -| `phoneNumbers` | `string`[] | [types.ts:237](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L237) | -| `tenantIds` | `string`[] | [types.ts:235](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L235) | -| `thirdParty` | `object`[] | [types.ts:238](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L238) | -| `timeJoined` | `number` | [types.ts:233](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L233) | -| `toJson` | () => `any` | [types.ts:257](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L257) | -| `webauthn` | `object` | [types.ts:242](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L242) | -| `webauthn.credentialIds` | `string`[] | [types.ts:243](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L243) | - -*** - -### UserContext - -```ts -type UserContext = Branded, "UserContext">; -``` - -Defined in: [types.ts:60](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L60) - -## Variables - -### nonPublicConfigProperties - -```ts -const nonPublicConfigProperties: readonly ["recipeList", "experimental"]; -``` - -Defined in: [types.ts:161](https://github.com/supertokens/supertokens-node/blob/8353a9a70d8b17dd9ff6ffe41201fa2ef0e60218/lib/ts/types.ts#L161) diff --git a/docs/references/backend-sdks/user-context.mdx b/docs/references/backend-sdks/user-context.mdx index c252f3a0ab..eb488ecd52 100644 --- a/docs/references/backend-sdks/user-context.mdx +++ b/docs/references/backend-sdks/user-context.mdx @@ -1,19 +1,11 @@ --- -id: user-context title: User Context -hide_title: true -sidebar_position: 6 -description: >- - Pass information across API functions using user context for custom execution - scenarios. -page_type: sdk-reference -category: backend-sdk +description: Pass information across API functions using user context for custom execution scenarios. +sidebar: + icon: contact + order: 40 --- - - -# User context - ## Overview The `UserContext` mechanism is a way to pass information across recipe or API functions to customize the authentication flow inside a specific *execution context*. @@ -22,7 +14,7 @@ By default, the user context passed to APIs and functions contains the request o ## Prerequisites -:::info Important +:::info[Important] This feature is only available for SDKs versions: - NodeJS >= `v9.0` - Python >= `v0.5` @@ -39,81 +31,79 @@ This is as opposed to it invoking from the sign in API, when it should continue To achieve this, all the API interface and recipe interface functions take a parameter called `userContext`, which is by default an empty object. When overriding the functions, anything can be added to this object, and that information carries onto the next set of functions called in the API. - - - + + ```tsx import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signUp: async function (input) { - let resp = await originalImplementation.signUp(input); - if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) { - /* - * This is called during the sign up API for email password login, - * but before calling the createNewSession function. - * We override the recipe function as shown here, - * and then set the relevant context only if it's a new user. - */ - input.userContext.isSignUp = true; - } - return resp; - }, - } - }, - } - // highlight-end - }), - ThirdParty.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - signInUp: async function (input) { - let resp = await originalImplementation.signInUp(input); - if (resp.status === "OK" && resp.createdNewRecipeUser && resp.user.loginMethods.length === 1 && input.session === undefined) { - /* - * This is called during the signInUp API for third party login, - * but before calling the createNewSession function. - * At the start of the API, we do not know if it will result in a - * sign in or a sign up, so we cannot override the API function. - * Instead, we override the recipe function as shown here, - * and then set the relevant context only if it's a new user. - */ - input.userContext.isSignUp = true; - } - return resp; - }, - } - }, - } - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + EmailPassword.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signUp: async function (input) { + let resp = await originalImplementation.signUp(input); + if (resp.status === "OK" && resp.user.loginMethods.length === 1 && input.session === undefined) { + /* + * This is called during the sign up API for email password login, + * but before calling the createNewSession function. + * We override the recipe function as shown here, + * and then set the relevant context only if it's a new user. + */ + input.userContext.isSignUp = true; + } + return resp; + }, + }; + }, + }, + }), + ThirdParty.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + signInUp: async function (input) { + let resp = await originalImplementation.signInUp(input); + if ( + resp.status === "OK" && + resp.createdNewRecipeUser && + resp.user.loginMethods.length === 1 && + input.session === undefined + ) { + /* + * This is called during the signInUp API for third party login, + * but before calling the createNewSession function. + * At the start of the API, we do not know if it will result in a + * sign in or a sign up, so we cannot override the API function. + * Instead, we override the recipe function as shown here, + * and then set the relevant context only if it's a new user. + */ + input.userContext.isSignUp = true; + } + return resp; + }, + }; + }, + }, + }), + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/emailpassword" @@ -127,7 +117,6 @@ func main() { supertokens.Init(supertokens.TypeInput{ RecipeList: []supertokens.Recipe{ thirdparty.Init(&tpmodels.TypeInput{ - // highlight-start Override: &tpmodels.OverrideStruct{ Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface { ogSignInUp := *originalImplementation.SignInUp @@ -156,10 +145,8 @@ func main() { return originalImplementation }, }, - // highlight-end }), emailpassword.Init(&epmodels.TypeInput{ - // highlight-start Override: &epmodels.OverrideStruct{ APIs: func(originalImplementation epmodels.APIInterface) epmodels.APIInterface { ogSignUpPOST := *originalImplementation.SignUpPOST @@ -177,17 +164,15 @@ func main() { return originalImplementation }, }, - // highlight-end }), }, }) } ``` - - - + + ```python -from supertokens_python import init, InputAppInfo +from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe import thirdparty, emailpassword from supertokens_python.recipe.thirdparty.interfaces import ( RecipeInterface, @@ -200,7 +185,6 @@ from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider from supertokens_python.recipe.session.interfaces import SessionContainer -# highlight-start def override_emailpassword_apis(original_implementation: APIInterface): original_sign_up_post = original_implementation.sign_up_post @@ -280,12 +264,10 @@ def override_thirdparty_functions(original_implementation: RecipeInterface): return original_implementation -# highlight-end - - init( + supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="fastapi", recipe_list=[ thirdparty.init( override=thirdparty.InputOverrideConfig( @@ -298,90 +280,88 @@ init( ], ) ``` - - + + Then consume that context in the `createNewSession` function to return an empty function in case the `userContext.isSignUp` is `true`. - - - + + ```tsx import SuperTokens from "supertokens-node"; -import ThirdParty from "supertokens-node/recipe/thirdparty" -import EmailPassword from "supertokens-node/recipe/emailpassword" +import ThirdParty from "supertokens-node/recipe/thirdparty"; +import EmailPassword from "supertokens-node/recipe/emailpassword"; import Session from "supertokens-node/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - supertokens: { - connectionURI: "...", - }, - recipeList: [ - ThirdParty.init({ - /* See previous step... */ - }), - EmailPassword.init({ - /* See previous step... */ - }), - Session.init({ - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - createNewSession: async function (input) { - if (input.userContext.isSignUp) { - /** - * The execution will come here only in case - * a sign up API is calling this function. This is because - * only then will the input.userContext.isSignUp === true - * (see above code). - */ - return { // this is an empty session. It won't result in a session being created for the user. - getAccessToken: () => "", - getAccessTokenPayload: () => null, - getExpiry: async () => -1, - getHandle: () => "", - getSessionDataFromDatabase: async () => null, - getTimeCreated: async () => -1, - getUserId: () => "", - revokeSession: async () => { }, - updateSessionDataInDatabase: async () => { }, - mergeIntoAccessTokenPayload: async () => { }, - assertClaims: async () => { }, - fetchAndSetClaim: async () => { }, - getClaimValue: async () => undefined, - setClaimValue: async () => { }, - removeClaim: async () => { }, - attachToRequestResponse: () => { }, - getAllSessionTokensDangerously: () => ({ - accessAndFrontTokenUpdated: false, - accessToken: "", - frontToken: "", - antiCsrfToken: undefined, - refreshToken: undefined, - }), - getTenantId: () => "public", - getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), - }; - } - return originalImplementation.createNewSession(input); - } - } - } - } - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + supertokens: { + connectionURI: "...", + }, + recipeList: [ + ThirdParty.init({ + /* See previous step... */ + }), + EmailPassword.init({ + /* See previous step... */ + }), + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + createNewSession: async function (input) { + if (input.userContext.isSignUp) { + /** + * The execution will come here only in case + * a sign up API is calling this function. This is because + * only then will the input.userContext.isSignUp === true + * (see above code). + */ + return { + // this is an empty session. It won't result in a session being created for the user. + getAccessToken: () => "", + getAccessTokenPayload: () => null, + getExpiry: async () => -1, + getHandle: () => "", + getSessionDataFromDatabase: async () => null, + getTimeCreated: async () => -1, + getUserId: () => "", + revokeSession: async () => {}, + updateSessionDataInDatabase: async () => {}, + mergeIntoAccessTokenPayload: async () => {}, + assertClaims: async () => {}, + fetchAndSetClaim: async () => {}, + getClaimValue: async () => undefined, + setClaimValue: async () => {}, + removeClaim: async () => {}, + attachToRequestResponse: () => {}, + getAllSessionTokensDangerously: () => ({ + accessAndFrontTokenUpdated: false, + accessToken: "", + frontToken: "", + antiCsrfToken: undefined, + refreshToken: undefined, + }), + getTenantId: () => "public", + getRecipeUserId: () => SuperTokens.convertToRecipeUserId(""), + }; + } + return originalImplementation.createNewSession(input); + }, + }; + }, + }, + }), + ], }); ``` - - - - + + ```go import ( "github.com/supertokens/supertokens-golang/recipe/session" @@ -462,11 +442,10 @@ func main() { }) } ``` - - - + + ```python -from supertokens_python import init, InputAppInfo +from supertokens_python import init, InputAppInfo, SupertokensConfig from supertokens_python.recipe.session.interfaces import ( RecipeInterface, SessionClaimValidator, @@ -482,8 +461,6 @@ from supertokens_python.types import RecipeUserId _T = TypeVar("_T") -# highlight-start - def override_session_functions(original_implementation: RecipeInterface): original_create_new_session = original_implementation.create_new_session @@ -518,12 +495,10 @@ def override_session_functions(original_implementation: RecipeInterface): return original_implementation -# highlight-end - - init( + supertokens_config=SupertokensConfig(connection_uri="..."), app_info=InputAppInfo(api_domain="...", app_name="...", website_domain="..."), - framework="...", # type: ignore + framework="fastapi", recipe_list=[ session.init( override=session.InputOverrideConfig(functions=override_session_functions) @@ -652,12 +627,11 @@ class EmptySession(session.SessionContainer): def get_tenant_id(self, user_context: Union[Dict[str, Any], None] = None) -> str: return "" ``` - - + + As a summary, when the sign up API invokes, the initial value of `userContext` is an empty object. Change that user context to add the `isSignUp` field, ensuring that information communicates to the `createNewSession` function. When that function invokes, it checks if `isSignUp === true`, and if it is, it doesn't call the original implementation, and instead, returns an empty session. This way, the system does not create a session if the user is signing up, but it creates one if the user is signing in. Note that there are other ways of achieving this, but the above showcases how user context can use to communicate across recipes and across API & Recipe functions. - diff --git a/docs/references/backend-sdks/user-object.mdx b/docs/references/backend-sdks/user-object.mdx index ae4799bd26..be43719e3c 100644 --- a/docs/references/backend-sdks/user-object.mdx +++ b/docs/references/backend-sdks/user-object.mdx @@ -1,23 +1,18 @@ --- title: User Object -hide_title: true -sidebar_position: 6 -description: >- - User Object page explains user structure, primary vs recipe user IDs, and - account linking. -page_type: sdk-reference -category: backend-sdk +description: User Object page explains user structure, primary vs recipe user IDs, and account linking. +sidebar: + icon: user + order: 50 --- -# The user object - ## Overview The user object represents the entity that exposes user information during an authentication flow. ## Prerequisites -:::info Important +:::info[Important] This is only applicable for NodeJS SDK >= 16.0 and for Python SDK >= 0.25.0. For other versions, or SDKs, please see: https://github.com/supertokens/core-driver-interface/wiki ::: @@ -63,52 +58,49 @@ This is only applicable for NodeJS SDK >= 16.0 and for Python SDK >= 0.25.0. For
- -## Code sample - + ```ts type User = { + id: string; + timeJoined: number; + isPrimaryUser: boolean; + tenantIds: string[]; + emails: string[]; + phoneNumbers: string[]; + thirdParty: { id: string; - timeJoined: number; - isPrimaryUser: boolean; + userId: string; + }[]; + loginMethods: { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; tenantIds: string[]; - emails: string[]; - phoneNumbers: string[]; - thirdParty: { - id: string; - userId: string; - }[]; - loginMethods: ({ - recipeId: "emailpassword" | "thirdparty" | "passwordless"; - tenantIds: string[]; - timeJoined: number; - recipeUserId: RecipeUserId; - verified: boolean; - email?: string; - phoneNumber?: string; - thirdParty?: { - id: string; - userId: string; - }; - hasSameEmailAs: (email: string | undefined) => boolean; - hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; - hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; - })[]; + timeJoined: number; + recipeUserId: RecipeUserId; + verified: boolean; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; + hasSameEmailAs: (email: string | undefined) => boolean; + hasSamePhoneNumberAs: (phoneNumber: string | undefined) => boolean; + hasSameThirdPartyInfoAs: (thirdParty?: { id: string; userId: string }) => boolean; + }[]; }; class RecipeUserId { - private recipeUserId: string; - constructor(recipeUserId: string) { - this.recipeUserId = recipeUserId; - } - - public getAsString = () => { - return this.recipeUserId; - }; + private recipeUserId: string; + constructor(recipeUserId: string) { + this.recipeUserId = recipeUserId; + } + + public getAsString = () => { + return this.recipeUserId; + }; } - ``` - + diff --git a/docs/references/cdi/_category_.json b/docs/references/cdi/_category_.json deleted file mode 100644 index a3516cd5a5..0000000000 --- a/docs/references/cdi/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Core Driver Interface", - "customProps": { "isMainCategory": true }, - "position": 5 -} diff --git a/docs/references/cdi/account-linking/_category_.json b/docs/references/cdi/account-linking/_category_.json deleted file mode 100644 index 6caee7b7ab..0000000000 --- a/docs/references/cdi/account-linking/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Account Linking", - "position": 1 -} diff --git a/docs/references/cdi/account-linking/get-accountlinking-user-link-check.mdx b/docs/references/cdi/account-linking/get-accountlinking-user-link-check.mdx deleted file mode 100644 index 0a3f64f27b..0000000000 --- a/docs/references/cdi/account-linking/get-accountlinking-user-link-check.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check account linking possibility -sidebar_label: get Check account linking possibility -sidebar_position: 1 -description: >- - GET /appid-{appId}/recipe/accountlinking/user/link/check endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/account-linking/get-accountlinking-user-primary-check.mdx b/docs/references/cdi/account-linking/get-accountlinking-user-primary-check.mdx deleted file mode 100644 index 439b65fd96..0000000000 --- a/docs/references/cdi/account-linking/get-accountlinking-user-primary-check.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check primary user creation possibility -sidebar_label: get Check primary user creation possibility -sidebar_position: 2 -description: >- - GET /appid-{appId}/recipe/accountlinking/user/primary/check endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/account-linking/meta.ts b/docs/references/cdi/account-linking/meta.ts new file mode 100644 index 0000000000..359b60a02e --- /dev/null +++ b/docs/references/cdi/account-linking/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Account Linking", + icon: "file-user", + order: 1, +}); diff --git a/docs/references/cdi/account-linking/post-accountlinking-user-link.mdx b/docs/references/cdi/account-linking/post-accountlinking-user-link.mdx deleted file mode 100644 index 54caca9e09..0000000000 --- a/docs/references/cdi/account-linking/post-accountlinking-user-link.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Link user accounts together -sidebar_label: post Link user accounts together -sidebar_position: 3 -description: >- - POST /appid-{appId}/recipe/accountlinking/user/link endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/account-linking/post-accountlinking-user-primary.mdx b/docs/references/cdi/account-linking/post-accountlinking-user-primary.mdx deleted file mode 100644 index d581ea6e94..0000000000 --- a/docs/references/cdi/account-linking/post-accountlinking-user-primary.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Create primary user account -sidebar_label: post Create primary user account -sidebar_position: 4 -description: >- - POST /appid-{appId}/recipe/accountlinking/user/primary endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/account-linking/post-accountlinking-user-unlink.mdx b/docs/references/cdi/account-linking/post-accountlinking-user-unlink.mdx deleted file mode 100644 index 2f37e598bc..0000000000 --- a/docs/references/cdi/account-linking/post-accountlinking-user-unlink.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Unlink user accounts -sidebar_label: post Unlink user accounts -sidebar_position: 5 -description: >- - POST /appid-{appId}/recipe/accountlinking/user/unlink endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/_category_.json b/docs/references/cdi/core/_category_.json deleted file mode 100644 index 4b7aa812e4..0000000000 --- a/docs/references/cdi/core/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Core", - "position": 3 -} diff --git a/docs/references/cdi/core/delete-ee-license.mdx b/docs/references/cdi/core/delete-ee-license.mdx deleted file mode 100644 index 53a2bee8b8..0000000000 --- a/docs/references/cdi/core/delete-ee-license.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete license key -sidebar_label: delete Delete license key -sidebar_position: 24 -description: 'DELETE `/appid-{appId}/ee/license` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/delete-hello.mdx b/docs/references/cdi/core/delete-hello.mdx deleted file mode 100644 index f931f2aa5e..0000000000 --- a/docs/references/cdi/core/delete-hello.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete hello message -sidebar_label: delete Delete hello message -sidebar_position: 23 -description: 'DELETE `/appid-{appId}/{tenantId}/hello` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-apiversion.mdx b/docs/references/cdi/core/get-apiversion.mdx deleted file mode 100644 index 51e28ff3cd..0000000000 --- a/docs/references/cdi/core/get-apiversion.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get API version -sidebar_label: get Get API version -sidebar_position: 6 -description: 'GET `/appid-{appId}/apiversion` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-config.mdx b/docs/references/cdi/core/get-config.mdx deleted file mode 100644 index ce5b186499..0000000000 --- a/docs/references/cdi/core/get-config.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get config file path -sidebar_label: get Get config file path -sidebar_position: 15 -description: GET `/config` endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-ee-featureflag.mdx b/docs/references/cdi/core/get-ee-featureflag.mdx deleted file mode 100644 index 2dd4e0ecf3..0000000000 --- a/docs/references/cdi/core/get-ee-featureflag.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get enterprise features -sidebar_label: get Get enterprise features -sidebar_position: 7 -description: 'GET `/appid-{appId}/ee/featureflag` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-ee-license.mdx b/docs/references/cdi/core/get-ee-license.mdx deleted file mode 100644 index 5b2696a9b6..0000000000 --- a/docs/references/cdi/core/get-ee-license.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get license key -sidebar_label: get Get license key -sidebar_position: 8 -description: 'GET `/appid-{appId}/ee/license` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-hello.mdx b/docs/references/cdi/core/get-hello.mdx deleted file mode 100644 index cec7b7af60..0000000000 --- a/docs/references/cdi/core/get-hello.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get hello message -sidebar_label: get Get hello message -sidebar_position: 2 -description: 'GET `/appid-{appId}/{tenantId}/hello` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-requests-stats.mdx b/docs/references/cdi/core/get-requests-stats.mdx deleted file mode 100644 index 94ea575362..0000000000 --- a/docs/references/cdi/core/get-requests-stats.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get requests stats -sidebar_label: get Get requests stats -sidebar_position: 10 -description: 'GET `/appid-{appId}/requests/stats` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-root.mdx b/docs/references/cdi/core/get-root.mdx deleted file mode 100644 index 5d6969f9c6..0000000000 --- a/docs/references/cdi/core/get-root.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get hello message on root path -sidebar_label: get Get hello message on root path -sidebar_position: 1 -description: 'GET `/appid-{appId}/{tenantId}/` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-telemetry.mdx b/docs/references/cdi/core/get-telemetry.mdx deleted file mode 100644 index a176f67c96..0000000000 --- a/docs/references/cdi/core/get-telemetry.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get telemetry ID -sidebar_label: get Get telemetry ID -sidebar_position: 11 -description: 'GET `/appid-{appId}/telemetry` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-user-id.mdx b/docs/references/cdi/core/get-user-id.mdx deleted file mode 100644 index 72c35b03b5..0000000000 --- a/docs/references/cdi/core/get-user-id.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get user ID -sidebar_label: get Get user ID -sidebar_position: 12 -description: 'GET `/appid-{appId}/user/id` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-user-search-tags.mdx b/docs/references/cdi/core/get-user-search-tags.mdx deleted file mode 100644 index b2d37c03dd..0000000000 --- a/docs/references/cdi/core/get-user-search-tags.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get search tags -sidebar_label: get Get search tags -sidebar_position: 13 -description: 'GET `/appid-{appId}/user/search/tags` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-userid-map.mdx b/docs/references/cdi/core/get-userid-map.mdx deleted file mode 100644 index 44ae6a0cd6..0000000000 --- a/docs/references/cdi/core/get-userid-map.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get user ID mapping -sidebar_label: get Get user ID mapping -sidebar_position: 9 -description: 'GET `/appid-{appId}/recipe/userid/map` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-users-by-accountinfo.mdx b/docs/references/cdi/core/get-users-by-accountinfo.mdx deleted file mode 100644 index 1747003622..0000000000 --- a/docs/references/cdi/core/get-users-by-accountinfo.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get users by account info -sidebar_label: get Get users by account info -sidebar_position: 4 -description: >- - GET `/appid-{appId}/{tenantId}/users/by-accountinfo` endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-users-count-active.mdx b/docs/references/cdi/core/get-users-count-active.mdx deleted file mode 100644 index fdd61f6522..0000000000 --- a/docs/references/cdi/core/get-users-count-active.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get active users count -sidebar_label: get Get active users count -sidebar_position: 14 -description: 'GET `/appid-{appId}/users/count/active` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-users-count.mdx b/docs/references/cdi/core/get-users-count.mdx deleted file mode 100644 index 451f1206a3..0000000000 --- a/docs/references/cdi/core/get-users-count.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get users count -sidebar_label: get Get users count -sidebar_position: 5 -description: 'GET `/appid-{appId}/{tenantId}/users/count` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/get-users.mdx b/docs/references/cdi/core/get-users.mdx deleted file mode 100644 index 79f840c034..0000000000 --- a/docs/references/cdi/core/get-users.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get users -sidebar_label: get Get users -sidebar_position: 3 -description: 'GET `/appid-{appId}/{tenantId}/users` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/meta.ts b/docs/references/cdi/core/meta.ts new file mode 100644 index 0000000000..e370f36171 --- /dev/null +++ b/docs/references/cdi/core/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Core", + icon: "cpu", + order: 3, +}); diff --git a/docs/references/cdi/core/post-hello.mdx b/docs/references/cdi/core/post-hello.mdx deleted file mode 100644 index 9175db22c0..0000000000 --- a/docs/references/cdi/core/post-hello.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Post hello message -sidebar_label: post Post hello message -sidebar_position: 16 -description: 'POST `/appid-{appId}/{tenantId}/hello` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/post-user-remove.mdx b/docs/references/cdi/core/post-user-remove.mdx deleted file mode 100644 index 8fcfaf0a3d..0000000000 --- a/docs/references/cdi/core/post-user-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete user -sidebar_label: post Delete user -sidebar_position: 19 -description: 'POST `/appid-{appId}/user/remove` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/post-userid-map-remove.mdx b/docs/references/cdi/core/post-userid-map-remove.mdx deleted file mode 100644 index b069e89bec..0000000000 --- a/docs/references/cdi/core/post-userid-map-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Remove user ID mapping -sidebar_label: post Remove user ID mapping -sidebar_position: 18 -description: 'POST `/appid-{appId}/recipe/userid/map/remove` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/post-userid-map.mdx b/docs/references/cdi/core/post-userid-map.mdx deleted file mode 100644 index 5b266adfa1..0000000000 --- a/docs/references/cdi/core/post-userid-map.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create user ID mapping -sidebar_label: post Create user ID mapping -sidebar_position: 17 -description: 'POST `/appid-{appId}/recipe/userid/map` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/put-ee-license.mdx b/docs/references/cdi/core/put-ee-license.mdx deleted file mode 100644 index 96482b617f..0000000000 --- a/docs/references/cdi/core/put-ee-license.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Set license key -sidebar_label: put Set license key -sidebar_position: 21 -description: 'PUT `/appid-{appId}/ee/license` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/put-hello.mdx b/docs/references/cdi/core/put-hello.mdx deleted file mode 100644 index 76aa70c913..0000000000 --- a/docs/references/cdi/core/put-hello.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Put hello message -sidebar_label: put Put hello message -sidebar_position: 20 -description: 'PUT `/appid-{appId}/{tenantId}/hello` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/core/put-userid-external-user-id-info.mdx b/docs/references/cdi/core/put-userid-external-user-id-info.mdx deleted file mode 100644 index 761df0e92d..0000000000 --- a/docs/references/cdi/core/put-userid-external-user-id-info.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Update external user info -sidebar_label: put Update external user info -sidebar_position: 22 -description: >- - PUT `/appid-{appId}/recipe/userid/external-user-id-info` endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/_category_.json b/docs/references/cdi/dashboard/_category_.json deleted file mode 100644 index 78534d5630..0000000000 --- a/docs/references/cdi/dashboard/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Dashboard", - "position": 4 -} diff --git a/docs/references/cdi/dashboard/delete-dashboard-session.mdx b/docs/references/cdi/dashboard/delete-dashboard-session.mdx deleted file mode 100644 index 5b4fb0f52a..0000000000 --- a/docs/references/cdi/dashboard/delete-dashboard-session.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Revoke dashboard user session -sidebar_label: delete Revoke dashboard user session -sidebar_position: 8 -description: 'DELETE `/appid-{appId}/recipe/dashboard/session` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/delete-dashboard-user.mdx b/docs/references/cdi/dashboard/delete-dashboard-user.mdx deleted file mode 100644 index 441ab8936c..0000000000 --- a/docs/references/cdi/dashboard/delete-dashboard-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete dashboard user -sidebar_label: delete Delete dashboard user -sidebar_position: 9 -description: 'DELETE `/appid-{appId}/recipe/dashboard/user` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/get-dashboard-tenant-core-config.mdx b/docs/references/cdi/dashboard/get-dashboard-tenant-core-config.mdx deleted file mode 100644 index c179d0c2ff..0000000000 --- a/docs/references/cdi/dashboard/get-dashboard-tenant-core-config.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get the core config -sidebar_label: get Get the core config -sidebar_position: 1 -description: >- - GET `/appid-{appId}/{tenantId}/recipe/dashboard/tenant/core-config` endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/get-dashboard-user-sessions.mdx b/docs/references/cdi/dashboard/get-dashboard-user-sessions.mdx deleted file mode 100644 index 71e86c28c8..0000000000 --- a/docs/references/cdi/dashboard/get-dashboard-user-sessions.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get all sessions for dashboard user -sidebar_label: get Get all sessions for dashboard user -sidebar_position: 2 -description: >- - GET `/appid-{appId}/recipe/dashboard/user/sessions` endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/get-dashboard-users.mdx b/docs/references/cdi/dashboard/get-dashboard-users.mdx deleted file mode 100644 index 74d3e12d32..0000000000 --- a/docs/references/cdi/dashboard/get-dashboard-users.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get all dashboard users -sidebar_label: get Get all dashboard users -sidebar_position: 3 -description: 'GET `/appid-{appId}/recipe/dashboard/users` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/meta.ts b/docs/references/cdi/dashboard/meta.ts new file mode 100644 index 0000000000..1f1524a6a2 --- /dev/null +++ b/docs/references/cdi/dashboard/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Dashboard", + icon: "layout-dashboard", + order: 4, +}); diff --git a/docs/references/cdi/dashboard/post-dashboard-session-verify.mdx b/docs/references/cdi/dashboard/post-dashboard-session-verify.mdx deleted file mode 100644 index e51812e77a..0000000000 --- a/docs/references/cdi/dashboard/post-dashboard-session-verify.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Verify dashboard user session -sidebar_label: post Verify dashboard user session -sidebar_position: 4 -description: >- - POST `/appid-{appId}/recipe/dashboard/session/verify` endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/post-dashboard-signin.mdx b/docs/references/cdi/dashboard/post-dashboard-signin.mdx deleted file mode 100644 index f489a70452..0000000000 --- a/docs/references/cdi/dashboard/post-dashboard-signin.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in dashboard user -sidebar_label: post Sign in dashboard user -sidebar_position: 5 -description: 'POST `/appid-{appId}/recipe/dashboard/signin` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/post-dashboard-user.mdx b/docs/references/cdi/dashboard/post-dashboard-user.mdx deleted file mode 100644 index c5a1a80d4f..0000000000 --- a/docs/references/cdi/dashboard/post-dashboard-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create dashboard user -sidebar_label: post Create dashboard user -sidebar_position: 6 -description: 'POST `/appid-{appId}/recipe/dashboard/user` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/dashboard/put-dashboard-user.mdx b/docs/references/cdi/dashboard/put-dashboard-user.mdx deleted file mode 100644 index d29dbe355f..0000000000 --- a/docs/references/cdi/dashboard/put-dashboard-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update dashboard user -sidebar_label: put Update dashboard user -sidebar_position: 7 -description: 'PUT `/appid-{appId}/recipe/dashboard/user` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/_category_.json b/docs/references/cdi/email-password/_category_.json deleted file mode 100644 index 93d1cf49e1..0000000000 --- a/docs/references/cdi/email-password/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Email/Password", - "position": 6 -} diff --git a/docs/references/cdi/email-password/get-user.mdx b/docs/references/cdi/email-password/get-user.mdx deleted file mode 100644 index d301b87953..0000000000 --- a/docs/references/cdi/email-password/get-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get email password user -sidebar_label: get Get email password user (deprecated) -sidebar_position: 1 -description: 'GET `/appid-{appId}/{tenantId}/recipe/user` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/meta.ts b/docs/references/cdi/email-password/meta.ts new file mode 100644 index 0000000000..29a9771589 --- /dev/null +++ b/docs/references/cdi/email-password/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email/Password", + icon: "rectangle-ellipsis", + order: 6, +}); diff --git a/docs/references/cdi/email-password/post-signin.mdx b/docs/references/cdi/email-password/post-signin.mdx deleted file mode 100644 index dc6a2b5bf5..0000000000 --- a/docs/references/cdi/email-password/post-signin.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in user -sidebar_label: post Sign in user -sidebar_position: 2 -description: 'POST `/appid-{appId}/{tenantId}/recipe/signin` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/post-signup.mdx b/docs/references/cdi/email-password/post-signup.mdx deleted file mode 100644 index 7f0fcda751..0000000000 --- a/docs/references/cdi/email-password/post-signup.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign up user -sidebar_label: post Sign up user -sidebar_position: 3 -description: 'POST `/appid-{appId}/{tenantId}/recipe/signup` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/post-user-password-reset-token-consume.mdx b/docs/references/cdi/email-password/post-user-password-reset-token-consume.mdx deleted file mode 100644 index 9dd015180c..0000000000 --- a/docs/references/cdi/email-password/post-user-password-reset-token-consume.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Consume password reset token -sidebar_label: post Consume password reset token -sidebar_position: 6 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/password/reset/token/consume - endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/post-user-password-reset-token.mdx b/docs/references/cdi/email-password/post-user-password-reset-token.mdx deleted file mode 100644 index 4cd5b80c8d..0000000000 --- a/docs/references/cdi/email-password/post-user-password-reset-token.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate password reset token -sidebar_label: post Generate password reset token -sidebar_position: 5 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/password/reset/token endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/post-user-password-reset.mdx b/docs/references/cdi/email-password/post-user-password-reset.mdx deleted file mode 100644 index 511bbefcdd..0000000000 --- a/docs/references/cdi/email-password/post-user-password-reset.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Reset user password -sidebar_label: post Reset user password (deprecated) -sidebar_position: 4 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/password/reset endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/post-user-passwordhash-import.mdx b/docs/references/cdi/email-password/post-user-passwordhash-import.mdx deleted file mode 100644 index 1ee55cecad..0000000000 --- a/docs/references/cdi/email-password/post-user-passwordhash-import.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Import user with hash -sidebar_label: post Import user with hash -sidebar_position: 7 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/passwordhash/import endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-password/put-user.mdx b/docs/references/cdi/email-password/put-user.mdx deleted file mode 100644 index fdd80a5b9c..0000000000 --- a/docs/references/cdi/email-password/put-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update user info -sidebar_label: put Update user info -sidebar_position: 8 -description: 'PUT /appid-{appId}/{tenantId}/recipe/user endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-verification/_category_.json b/docs/references/cdi/email-verification/_category_.json deleted file mode 100644 index c47c04104f..0000000000 --- a/docs/references/cdi/email-verification/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Email Verification", - "position": 5 -} diff --git a/docs/references/cdi/email-verification/get-user-email-verify.mdx b/docs/references/cdi/email-verification/get-user-email-verify.mdx deleted file mode 100644 index 4aa82040c8..0000000000 --- a/docs/references/cdi/email-verification/get-user-email-verify.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Check email verification -sidebar_label: get Check email verification -sidebar_position: 1 -description: 'GET /appid-{appId}/recipe/user/email/verify endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-verification/meta.ts b/docs/references/cdi/email-verification/meta.ts new file mode 100644 index 0000000000..d9760f56aa --- /dev/null +++ b/docs/references/cdi/email-verification/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email Verification", + icon: "mail-check", + order: 5, +}); diff --git a/docs/references/cdi/email-verification/post-user-email-verify-remove.mdx b/docs/references/cdi/email-verification/post-user-email-verify-remove.mdx deleted file mode 100644 index 6013d0add7..0000000000 --- a/docs/references/cdi/email-verification/post-user-email-verify-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Unverify email -sidebar_label: post Unverify email -sidebar_position: 5 -description: >- - POST /appid-{appId}/recipe/user/email/verify/remove endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-verification/post-user-email-verify-token-remove.mdx b/docs/references/cdi/email-verification/post-user-email-verify-token-remove.mdx deleted file mode 100644 index d8e63395e9..0000000000 --- a/docs/references/cdi/email-verification/post-user-email-verify-token-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove email verification tokens -sidebar_label: post Remove email verification tokens -sidebar_position: 4 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/email/verify/token/remove endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-verification/post-user-email-verify-token.mdx b/docs/references/cdi/email-verification/post-user-email-verify-token.mdx deleted file mode 100644 index ec4ba9c95c..0000000000 --- a/docs/references/cdi/email-verification/post-user-email-verify-token.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate email verification token -sidebar_label: post Generate email verification token -sidebar_position: 3 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/email/verify/token endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/email-verification/post-user-email-verify.mdx b/docs/references/cdi/email-verification/post-user-email-verify.mdx deleted file mode 100644 index 7065810bb2..0000000000 --- a/docs/references/cdi/email-verification/post-user-email-verify.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Verify email -sidebar_label: post Verify email -sidebar_position: 2 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/email/verify endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/import/_category_.json b/docs/references/cdi/import/_category_.json deleted file mode 100644 index 8cda0027b4..0000000000 --- a/docs/references/cdi/import/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Bulk Import", - "position": 2 -} diff --git a/docs/references/cdi/import/delete-bulk-import-users.mdx b/docs/references/cdi/import/delete-bulk-import-users.mdx deleted file mode 100644 index 36efd77b64..0000000000 --- a/docs/references/cdi/import/delete-bulk-import-users.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete bulk import users -sidebar_label: post Delete bulk import users -sidebar_position: 4 -description: 'POST /appid-{appId}/bulk-import/users/remove endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/import/get-bulk-import-users-count.mdx b/docs/references/cdi/import/get-bulk-import-users-count.mdx deleted file mode 100644 index a033ec1030..0000000000 --- a/docs/references/cdi/import/get-bulk-import-users-count.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Import one user directly -sidebar_label: post Import one user directly -sidebar_position: 5 -description: POST /appid-/bulk-import/import endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/import/get-bulk-import-users.mdx b/docs/references/cdi/import/get-bulk-import-users.mdx deleted file mode 100644 index cade60590b..0000000000 --- a/docs/references/cdi/import/get-bulk-import-users.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List bulk import users -sidebar_label: get List bulk import users -sidebar_position: 1 -description: 'GET /appid-{appId}/bulk-import/users endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/import/meta.ts b/docs/references/cdi/import/meta.ts new file mode 100644 index 0000000000..ddf2a0a6ef --- /dev/null +++ b/docs/references/cdi/import/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Bulk Import", + icon: "user-plus", + order: 2, +}); diff --git a/docs/references/cdi/import/post-bulk-import-import.mdx b/docs/references/cdi/import/post-bulk-import-import.mdx deleted file mode 100644 index fb34c16416..0000000000 --- a/docs/references/cdi/import/post-bulk-import-import.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Count bulk import users -sidebar_label: get Count bulk import users -sidebar_position: 2 -description: GET /appid-/bulk-import/users/count endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/import/post-bulk-import-users.mdx b/docs/references/cdi/import/post-bulk-import-users.mdx deleted file mode 100644 index 9dd0a52027..0000000000 --- a/docs/references/cdi/import/post-bulk-import-users.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Add bulk import users -sidebar_label: post Add bulk import users -sidebar_position: 3 -description: 'POST /appid-{appId}/bulk-import/users endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/introduction.mdx b/docs/references/cdi/introduction.mdx index cfcef3ccfb..142b47384a 100644 --- a/docs/references/cdi/introduction.mdx +++ b/docs/references/cdi/introduction.mdx @@ -1,30 +1,20 @@ --- title: Introduction -hide_title: true -sidebar_position: 0.1 -description: >- - Access detailed API specifications for integrating with SuperTokens Core and - frontend SDKs. -pagination_prev: null -pagination_next: null -page_type: overview -category: cdi +description: Access detailed API specifications for integrating with SuperTokens Core and frontend SDKs. +sidebar: + order: 0.1 --- -# Core Driver Interface - - ## Overview The **CDI**, Core Driver Interface, is the API exposed by the **SuperTokens Core** service. It is meant to be consumed only by your backend only. -:::info no-title -In most cases, you don't need to directly interact with the API, since the existing [backend SDKS](/docs/references/backend-sdks/reference) are built on top of it. +:::info +In most cases, you don't need to directly interact with the API, since the existing [backend SDKS](/references/backend-sdks/reference) are built on top of it. ::: - ### URL Structure Most of the endpoints take in two path parameters: `appid-{appId}` and `{tenantId}`. @@ -55,8 +45,4 @@ In order for the core service to accept a request you need to set the `api-key` If you are using the managed service, the token can be accessed from the **Connection Information** section on the deployment's **Overview** page in [the SaaS Dashboard](https://supertokens.com/dashboard). In the context of a self-hosted instance, keys are not created by default. -You have to explicitly [generate them](/docs/platform-configuration/supertokens-core/api-keys). - - - - +You have to explicitly [generate them](/platform-configuration/supertokens-core/api-keys). diff --git a/docs/references/cdi/meta.ts b/docs/references/cdi/meta.ts new file mode 100644 index 0000000000..bce90f251b --- /dev/null +++ b/docs/references/cdi/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Core Driver Interface", + icon: "file-terminal", + order: 70, +}); diff --git a/docs/references/cdi/mfa/_category_.json b/docs/references/cdi/mfa/_category_.json deleted file mode 100644 index 6912806378..0000000000 --- a/docs/references/cdi/mfa/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "MFA", - "position": 7 -} diff --git a/docs/references/cdi/mfa/get-totp-device-list.mdx b/docs/references/cdi/mfa/get-totp-device-list.mdx deleted file mode 100644 index 6cd8e03f49..0000000000 --- a/docs/references/cdi/mfa/get-totp-device-list.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List user TOTP devices -sidebar_label: get List user TOTP devices -sidebar_position: 1 -description: 'GET /appid-{appId}/recipe/totp/device/list endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/meta.ts b/docs/references/cdi/mfa/meta.ts new file mode 100644 index 0000000000..bf6a311c74 --- /dev/null +++ b/docs/references/cdi/mfa/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "MFA", + icon: "shield-check", + order: 7, +}); diff --git a/docs/references/cdi/mfa/post-totp-device-import.mdx b/docs/references/cdi/mfa/post-totp-device-import.mdx deleted file mode 100644 index ca34bd640a..0000000000 --- a/docs/references/cdi/mfa/post-totp-device-import.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Import existing TOTP device -sidebar_label: post Import existing TOTP device -sidebar_position: 6 -description: POST /recipe/totp/device/import endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/post-totp-device-remove.mdx b/docs/references/cdi/mfa/post-totp-device-remove.mdx deleted file mode 100644 index 7a0d85b5f1..0000000000 --- a/docs/references/cdi/mfa/post-totp-device-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Remove TOTP device -sidebar_label: post Remove TOTP device -sidebar_position: 5 -description: 'POST /appid-{appId}/recipe/totp/device/remove endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/post-totp-device-verify.mdx b/docs/references/cdi/mfa/post-totp-device-verify.mdx deleted file mode 100644 index 6bcef8ffcb..0000000000 --- a/docs/references/cdi/mfa/post-totp-device-verify.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Verify TOTP device -sidebar_label: post Verify TOTP device -sidebar_position: 2 -description: >- - POST /appid-{appId}/{tenantId}/recipe/totp/device/verify endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/post-totp-device.mdx b/docs/references/cdi/mfa/post-totp-device.mdx deleted file mode 100644 index 5a49941191..0000000000 --- a/docs/references/cdi/mfa/post-totp-device.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Add TOTP device for user -sidebar_label: post Add TOTP device for user -sidebar_position: 4 -description: 'POST /appid-{appId}/recipe/totp/device endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/post-totp-verify.mdx b/docs/references/cdi/mfa/post-totp-verify.mdx deleted file mode 100644 index 194f408524..0000000000 --- a/docs/references/cdi/mfa/post-totp-verify.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Verify TOTP code -sidebar_label: post Verify TOTP code -sidebar_position: 3 -description: >- - POST /appid-{appId}/{tenantId}/recipe/totp/verify endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/mfa/put-totp-device.mdx b/docs/references/cdi/mfa/put-totp-device.mdx deleted file mode 100644 index 4316fe10c5..0000000000 --- a/docs/references/cdi/mfa/put-totp-device.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update TOTP device name -sidebar_label: put Update TOTP device name -sidebar_position: 7 -description: 'PUT /appid-{appId}/recipe/totp/device endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/_category_.json b/docs/references/cdi/multitenancy/_category_.json deleted file mode 100644 index 438459d13e..0000000000 --- a/docs/references/cdi/multitenancy/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Multitenancy", - "position": 8 -} diff --git a/docs/references/cdi/multitenancy/get-multitenancy-app-list-v2.mdx b/docs/references/cdi/multitenancy/get-multitenancy-app-list-v2.mdx deleted file mode 100644 index 85b279b3f8..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-app-list-v2.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List apps -sidebar_label: get List apps -sidebar_position: 6 -description: GET /recipe/multitenancy/app/list/v2 endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-app-list.mdx b/docs/references/cdi/multitenancy/get-multitenancy-app-list.mdx deleted file mode 100644 index 8b642c5455..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-app-list.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List apps -sidebar_label: get List apps (deprecated) -sidebar_position: 5 -description: GET /recipe/multitenancy/app/list endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list-v2.mdx b/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list-v2.mdx deleted file mode 100644 index 85edd56da4..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list-v2.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List connection uri domains -sidebar_label: get List connection uri domains -sidebar_position: 8 -description: >- - GET /recipe/multitenancy/connectionuridomain/list/v2 endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list.mdx b/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list.mdx deleted file mode 100644 index 5025bc4ee1..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-connectionuridomain-list.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List connection uri domains -sidebar_label: get List connection uri domains (deprecated) -sidebar_position: 7 -description: >- - GET /recipe/multitenancy/connectionuridomain/list endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-tenant-list-v2.mdx b/docs/references/cdi/multitenancy/get-multitenancy-tenant-list-v2.mdx deleted file mode 100644 index e77318415b..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-tenant-list-v2.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List tenants in an app -sidebar_label: get List tenants in an app -sidebar_position: 4 -description: >- - GET /appid-{appId}/recipe/multitenancy/tenant/list/v2 endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-tenant-list.mdx b/docs/references/cdi/multitenancy/get-multitenancy-tenant-list.mdx deleted file mode 100644 index 8380f05bc5..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-tenant-list.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List tenants in an app -sidebar_label: get List tenants in an app (deprecated) -sidebar_position: 3 -description: >- - GET /appid-{appId}/recipe/multitenancy/tenant/list endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-tenant-v2.mdx b/docs/references/cdi/multitenancy/get-multitenancy-tenant-v2.mdx deleted file mode 100644 index af7a6dd864..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-tenant-v2.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get tenant configuration -sidebar_label: get Get tenant configuration -sidebar_position: 2 -description: >- - GET /appid-{appId}/{tenantId}/recipe/multitenancy/tenant/v2 endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/get-multitenancy-tenant.mdx b/docs/references/cdi/multitenancy/get-multitenancy-tenant.mdx deleted file mode 100644 index 174992f6cb..0000000000 --- a/docs/references/cdi/multitenancy/get-multitenancy-tenant.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get tenant configuration -sidebar_label: get Get tenant configuration (deprecated) -sidebar_position: 1 -description: >- - GET /appid-{appId}/{tenantId}/recipe/multitenancy/tenant endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/meta.ts b/docs/references/cdi/multitenancy/meta.ts new file mode 100644 index 0000000000..c8945a5ef2 --- /dev/null +++ b/docs/references/cdi/multitenancy/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Multitenancy", + icon: "house-plus", + order: 8, +}); diff --git a/docs/references/cdi/multitenancy/post-multitenancy-app-remove.mdx b/docs/references/cdi/multitenancy/post-multitenancy-app-remove.mdx deleted file mode 100644 index dcec5e7396..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-app-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete app -sidebar_label: post Delete app -sidebar_position: 13 -description: POST /recipe/multitenancy/app/remove endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/post-multitenancy-config-thirdparty-remove.mdx b/docs/references/cdi/multitenancy/post-multitenancy-config-thirdparty-remove.mdx deleted file mode 100644 index d42283c937..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-config-thirdparty-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Delete ThirdParty Provider configuration -sidebar_label: post Delete ThirdParty Provider configuration -sidebar_position: 9 -description: >- - POST /appid-{appId}/{tenantId}/recipe/multitenancy/config/thirdparty/remove - endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/post-multitenancy-connectionuridomain-remove.mdx b/docs/references/cdi/multitenancy/post-multitenancy-connectionuridomain-remove.mdx deleted file mode 100644 index bf4ab3b4d4..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-connectionuridomain-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove connection uri domain -sidebar_label: post Remove connection uri domain -sidebar_position: 14 -description: >- - POST /recipe/multitenancy/connectionuridomain/remove endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/post-multitenancy-tenant-remove.mdx b/docs/references/cdi/multitenancy/post-multitenancy-tenant-remove.mdx deleted file mode 100644 index 5229a2869b..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-tenant-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Delete a tenant -sidebar_label: post Delete a tenant -sidebar_position: 12 -description: >- - POST /appid-{appId}/recipe/multitenancy/tenant/remove endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/post-multitenancy-tenant-user-remove.mdx b/docs/references/cdi/multitenancy/post-multitenancy-tenant-user-remove.mdx deleted file mode 100644 index 072d599710..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-tenant-user-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove user tenant association -sidebar_label: post Remove user tenant association -sidebar_position: 11 -description: >- - POST /appid-{appId}/{tenantId}/recipe/multitenancy/tenant/user/remove endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/post-multitenancy-tenant-user.mdx b/docs/references/cdi/multitenancy/post-multitenancy-tenant-user.mdx deleted file mode 100644 index 029b3301de..0000000000 --- a/docs/references/cdi/multitenancy/post-multitenancy-tenant-user.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Add user tenant association -sidebar_label: post Add user tenant association -sidebar_position: 10 -description: >- - POST /appid-{appId}/{tenantId}/recipe/multitenancy/tenant/user endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-app-v2.mdx b/docs/references/cdi/multitenancy/put-multitenancy-app-v2.mdx deleted file mode 100644 index 7b112f4700..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-app-v2.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Upsert app -sidebar_label: put Upsert app -sidebar_position: 19 -description: PUT /recipe/multitenancy/app/v2 endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-app.mdx b/docs/references/cdi/multitenancy/put-multitenancy-app.mdx deleted file mode 100644 index 0e0353d868..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-app.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Upsert app -sidebar_label: put Upsert app (deprecated) -sidebar_position: 18 -description: PUT /recipe/multitenancy/app endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-config-thirdparty.mdx b/docs/references/cdi/multitenancy/put-multitenancy-config-thirdparty.mdx deleted file mode 100644 index a5eff3e1fd..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-config-thirdparty.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Upsert ThirdParty Provider configuration -sidebar_label: put Upsert ThirdParty Provider configuration -sidebar_position: 15 -description: >- - PUT /appid-{appId}/{tenantId}/recipe/multitenancy/config/thirdparty endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain-v2.mdx b/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain-v2.mdx deleted file mode 100644 index d6a1b45405..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain-v2.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Upsert connection uri domain -sidebar_label: put Upsert connection uri domain -sidebar_position: 21 -description: >- - PUT /recipe/multitenancy/connectionuridomain/v2 endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain.mdx b/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain.mdx deleted file mode 100644 index f57be05b71..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-connectionuridomain.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Upsert connection URI domain -sidebar_label: put Upsert connection URI domain (deprecated) -sidebar_position: 20 -description: PUT /recipe/multitenancy/connectionuridomain endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-tenant-v2.mdx b/docs/references/cdi/multitenancy/put-multitenancy-tenant-v2.mdx deleted file mode 100644 index 9a3dbe8e82..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-tenant-v2.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Upsert tenant -sidebar_label: put Upsert tenant -sidebar_position: 17 -description: >- - PUT /appid-{appId}/recipe/multitenancy/tenant/v2 endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/multitenancy/put-multitenancy-tenant.mdx b/docs/references/cdi/multitenancy/put-multitenancy-tenant.mdx deleted file mode 100644 index ae192fd767..0000000000 --- a/docs/references/cdi/multitenancy/put-multitenancy-tenant.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Upsert tenant -sidebar_label: put Upsert tenant (deprecated) -sidebar_position: 16 -description: 'PUT /appid-{appId}/recipe/multitenancy/tenant endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/_category_.json b/docs/references/cdi/oauth/_category_.json deleted file mode 100644 index 7644618425..0000000000 --- a/docs/references/cdi/oauth/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "OAuth", - "position": 9 -} diff --git a/docs/references/cdi/oauth/get-jwks.mdx b/docs/references/cdi/oauth/get-jwks.mdx deleted file mode 100644 index 7ec2141554..0000000000 --- a/docs/references/cdi/oauth/get-jwks.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get JWT keys -sidebar_label: get Get JWT keys (deprecated) -sidebar_position: 2 -description: 'GET /appid-{appId}/recipe/jwt/jwks endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-auth.mdx b/docs/references/cdi/oauth/get-oauth2-auth.mdx deleted file mode 100644 index f02d33afd0..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-auth.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get OAuth2 Auth -sidebar_label: get Get OAuth2 Auth -sidebar_position: 3 -description: 'GET /appid-{appId}/recipe/oauth/auth endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-client.mdx b/docs/references/cdi/oauth/get-oauth2-client.mdx deleted file mode 100644 index f5286d2ca3..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-client.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get OAuth2 Client -sidebar_label: get Get OAuth2 Client -sidebar_position: 6 -description: 'GET /appid-{appId}/recipe/oauth/clients endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-clients.mdx b/docs/references/cdi/oauth/get-oauth2-clients.mdx deleted file mode 100644 index 90142f304a..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-clients.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List OAuth2 Clients -sidebar_label: get List OAuth2 Clients -sidebar_position: 7 -description: 'GET /appid-{appId}/recipe/oauth/clients/list endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-consent-request.mdx b/docs/references/cdi/oauth/get-oauth2-consent-request.mdx deleted file mode 100644 index d6e7982623..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-consent-request.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get OAuth2 Consent Request -sidebar_label: get Get OAuth2 Consent Request -sidebar_position: 4 -description: >- - GET /appid-{appId}/recipe/oauth/auth/requests/consent endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-login-request.mdx b/docs/references/cdi/oauth/get-oauth2-login-request.mdx deleted file mode 100644 index 48e64edd94..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-login-request.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get OAuth2 Login Request -sidebar_label: get Get OAuth2 Login Request -sidebar_position: 5 -description: >- - GET /appid-{appId}/recipe/oauth/auth/requests/login endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-sessions-logout.mdx b/docs/references/cdi/oauth/get-oauth2-sessions-logout.mdx deleted file mode 100644 index 291713dd3e..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-sessions-logout.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get OAuth2 Sessions Logout -sidebar_label: get Get OAuth2 Sessions Logout -sidebar_position: 8 -description: >- - GET /appid-{appId}/recipe/oauth/sessions/logout endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-oauth2-token.mdx b/docs/references/cdi/oauth/get-oauth2-token.mdx deleted file mode 100644 index 3e9e3d2f43..0000000000 --- a/docs/references/cdi/oauth/get-oauth2-token.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get OAuth2 Token -sidebar_label: post Get OAuth2 Token -sidebar_position: 13 -description: 'POST /appid-{appId}/recipe/oauth/token endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/get-well-known-jwks.mdx b/docs/references/cdi/oauth/get-well-known-jwks.mdx deleted file mode 100644 index 62ecb4d092..0000000000 --- a/docs/references/cdi/oauth/get-well-known-jwks.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get well-known JWT keys -sidebar_label: get Get well-known JWT keys -sidebar_position: 1 -description: 'GET /appid-{appId}/.well-known/jwks.json endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/meta.ts b/docs/references/cdi/oauth/meta.ts new file mode 100644 index 0000000000..82c0edf015 --- /dev/null +++ b/docs/references/cdi/oauth/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "OAuth", + icon: "key-round", + order: 9, +}); diff --git a/docs/references/cdi/oauth/post-oauth2-client-remove.mdx b/docs/references/cdi/oauth/post-oauth2-client-remove.mdx deleted file mode 100644 index 48f8f92650..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-client-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove OAuth2 Client -sidebar_label: post Remove OAuth2 Client -sidebar_position: 10 -description: >- - POST /appid-{appId}/recipe/oauth/clients/remove endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-client.mdx b/docs/references/cdi/oauth/post-oauth2-client.mdx deleted file mode 100644 index bbf0fb4c05..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-client.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create OAuth2 Client -sidebar_label: post Create OAuth2 Client -sidebar_position: 9 -description: 'POST /appid-{appId}/recipe/oauth/clients endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-consent-request-accept.mdx b/docs/references/cdi/oauth/post-oauth2-consent-request-accept.mdx deleted file mode 100644 index be5e47af42..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-consent-request-accept.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Accept OAuth2 Consent Request -sidebar_label: put Accept OAuth2 Consent Request -sidebar_position: 16 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/consent/accept endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-consent-request-reject.mdx b/docs/references/cdi/oauth/post-oauth2-consent-request-reject.mdx deleted file mode 100644 index d63bc2791a..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-consent-request-reject.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Reject OAuth2 Consent Request -sidebar_label: put Reject OAuth2 Consent Request -sidebar_position: 17 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/consent/reject endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-login-request-accept.mdx b/docs/references/cdi/oauth/post-oauth2-login-request-accept.mdx deleted file mode 100644 index fa99f5b352..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-login-request-accept.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Accept OAuth2 Login Request -sidebar_label: put Accept OAuth2 Login Request -sidebar_position: 18 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/login/accept endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-login-request-reject.mdx b/docs/references/cdi/oauth/post-oauth2-login-request-reject.mdx deleted file mode 100644 index 0f79d699a4..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-login-request-reject.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Reject OAuth2 Login Request -sidebar_label: put Reject OAuth2 Login Request -sidebar_position: 19 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/login/reject endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-logout-request-accept.mdx b/docs/references/cdi/oauth/post-oauth2-logout-request-accept.mdx deleted file mode 100644 index d3b4491ea4..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-logout-request-accept.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Accept OAuth2 Logout Request -sidebar_label: put Accept OAuth2 Logout Request -sidebar_position: 20 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/logout/accept endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-logout-request-reject.mdx b/docs/references/cdi/oauth/post-oauth2-logout-request-reject.mdx deleted file mode 100644 index 0ece7aa2d2..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-logout-request-reject.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Reject OAuth2 Logout Request -sidebar_label: put Reject OAuth2 Logout Request -sidebar_position: 21 -description: >- - PUT /appid-{appId}/recipe/oauth/auth/requests/logout/reject endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-session-revoke.mdx b/docs/references/cdi/oauth/post-oauth2-session-revoke.mdx deleted file mode 100644 index 54269159c4..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-session-revoke.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Revoke OAuth2 Session -sidebar_label: post Revoke OAuth2 Session -sidebar_position: 12 -description: >- - POST /appid-{appId}/recipe/oauth/session/revoke endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-token-introspect.mdx b/docs/references/cdi/oauth/post-oauth2-token-introspect.mdx deleted file mode 100644 index 881734431f..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-token-introspect.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Introspect OAuth2 Token -sidebar_label: post Introspect OAuth2 Token -sidebar_position: 11 -description: 'POST /appid-{appId}/recipe/oauth/introspect endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-token-revoke.mdx b/docs/references/cdi/oauth/post-oauth2-token-revoke.mdx deleted file mode 100644 index d74b279845..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-token-revoke.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Revoke OAuth2 Token -sidebar_label: post Revoke OAuth2 Token -sidebar_position: 14 -description: 'POST /appid-{appId}/recipe/oauth/token/revoke endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/post-oauth2-tokens-revoke.mdx b/docs/references/cdi/oauth/post-oauth2-tokens-revoke.mdx deleted file mode 100644 index 7f1aa4e42c..0000000000 --- a/docs/references/cdi/oauth/post-oauth2-tokens-revoke.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Revoke OAuth2 Token -sidebar_label: post Revoke OAuth2 Token -sidebar_position: 15 -description: 'POST /appid-{appId}/recipe/oauth/tokens/revoke endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/oauth/put-oauth2-client.mdx b/docs/references/cdi/oauth/put-oauth2-client.mdx deleted file mode 100644 index 91e70d0be7..0000000000 --- a/docs/references/cdi/oauth/put-oauth2-client.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update OAuth2 Client -sidebar_label: put Update OAuth2 Client -sidebar_position: 22 -description: 'PUT /appid-{appId}/recipe/oauth/clients endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/_category_.json b/docs/references/cdi/passwordless/_category_.json deleted file mode 100644 index 12cacdc38f..0000000000 --- a/docs/references/cdi/passwordless/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Passwordless", - "position": 10 -} diff --git a/docs/references/cdi/passwordless/get-signinup-codes.mdx b/docs/references/cdi/passwordless/get-signinup-codes.mdx deleted file mode 100644 index d48719f07b..0000000000 --- a/docs/references/cdi/passwordless/get-signinup-codes.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List passwordless codes -sidebar_label: get List passwordless codes -sidebar_position: 1 -description: >- - GET /appid-{appId}/{tenantId}/recipe/signinup/codes endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/get-user.mdx b/docs/references/cdi/passwordless/get-user.mdx deleted file mode 100644 index f5063cec9c..0000000000 --- a/docs/references/cdi/passwordless/get-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get passwordless user -sidebar_label: get Get passwordless user (deprecated) -sidebar_position: 2 -description: 'GET /appid-{appId}/{tenantId}/recipe/user ⠀ endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/meta.ts b/docs/references/cdi/passwordless/meta.ts new file mode 100644 index 0000000000..169bfad0ee --- /dev/null +++ b/docs/references/cdi/passwordless/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Passwordless", + icon: "wand-sparkles", + order: 10, +}); diff --git a/docs/references/cdi/passwordless/post-signinup-code-check.mdx b/docs/references/cdi/passwordless/post-signinup-code-check.mdx deleted file mode 100644 index 0dbeee6777..0000000000 --- a/docs/references/cdi/passwordless/post-signinup-code-check.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check passwordless code -sidebar_label: post Check passwordless code -sidebar_position: 4 -description: >- - POST /appid-{appId}/{tenantId}/recipe/signinup/code/check endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/post-signinup-code-consume.mdx b/docs/references/cdi/passwordless/post-signinup-code-consume.mdx deleted file mode 100644 index fdc5d9310d..0000000000 --- a/docs/references/cdi/passwordless/post-signinup-code-consume.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Consume passwordless code -sidebar_label: post Consume passwordless code -sidebar_position: 5 -description: >- - POST /appid-{appId}/{tenantId}/recipe/signinup/code/consume endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/post-signinup-code-remove.mdx b/docs/references/cdi/passwordless/post-signinup-code-remove.mdx deleted file mode 100644 index 71cc60e2b3..0000000000 --- a/docs/references/cdi/passwordless/post-signinup-code-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Revoke passwordless code -sidebar_label: post Revoke passwordless code -sidebar_position: 6 -description: >- - POST /appid-{appId}/{tenantId}/recipe/signinup/code/remove endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/post-signinup-code.mdx b/docs/references/cdi/passwordless/post-signinup-code.mdx deleted file mode 100644 index 8a039ffdb9..0000000000 --- a/docs/references/cdi/passwordless/post-signinup-code.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Start passwordless sign in -sidebar_label: post Start passwordless sign in -sidebar_position: 3 -description: >- - POST /appid-{appId}/{tenantId}/recipe/signinup/code endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/post-signinup-codes-remove.mdx b/docs/references/cdi/passwordless/post-signinup-codes-remove.mdx deleted file mode 100644 index f1bbff496f..0000000000 --- a/docs/references/cdi/passwordless/post-signinup-codes-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Revoke all user codes -sidebar_label: post Revoke all user codes -sidebar_position: 7 -description: >- - POST /appid-{appId}/{tenantId}/recipe/signinup/codes/remove endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/passwordless/put-user.mdx b/docs/references/cdi/passwordless/put-user.mdx deleted file mode 100644 index b26473eb87..0000000000 --- a/docs/references/cdi/passwordless/put-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update passwordless user -sidebar_label: put Update passwordless user -sidebar_position: 8 -description: 'PUT /appid-{appId}/{tenantId}/recipe/user ⠀ endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/_category_.json b/docs/references/cdi/session/_category_.json deleted file mode 100644 index afa4a5a155..0000000000 --- a/docs/references/cdi/session/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Session", - "position": 11 -} diff --git a/docs/references/cdi/session/get-jwt-data.mdx b/docs/references/cdi/session/get-jwt-data.mdx deleted file mode 100644 index 96ce5b664b..0000000000 --- a/docs/references/cdi/session/get-jwt-data.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get JWT data -sidebar_label: get Get JWT data (deprecated) -sidebar_position: 1 -description: 'GET `/appid-{appId}/{tenantId}/recipe/jwt/data` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/get-session-data.mdx b/docs/references/cdi/session/get-session-data.mdx deleted file mode 100644 index e393595e97..0000000000 --- a/docs/references/cdi/session/get-session-data.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get session data -sidebar_label: get Get session data (deprecated) -sidebar_position: 4 -description: 'GET `/appid-{appId}/recipe/session/data` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/get-session-user.mdx b/docs/references/cdi/session/get-session-user.mdx deleted file mode 100644 index 791dd10b69..0000000000 --- a/docs/references/cdi/session/get-session-user.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get user session handles -sidebar_label: get Get user session handles -sidebar_position: 2 -description: >- - GET /appid-{appId}/{tenantId}/recipe/session/user endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/get-session.mdx b/docs/references/cdi/session/get-session.mdx deleted file mode 100644 index 48d3fb8fc3..0000000000 --- a/docs/references/cdi/session/get-session.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get session info -sidebar_label: get Get session info -sidebar_position: 3 -description: 'GET `/appid-{appId}/recipe/session` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/meta.ts b/docs/references/cdi/session/meta.ts new file mode 100644 index 0000000000..a5becb114c --- /dev/null +++ b/docs/references/cdi/session/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Session", + icon: "clock-3", + order: 11, +}); diff --git a/docs/references/cdi/session/post-jwt.mdx b/docs/references/cdi/session/post-jwt.mdx deleted file mode 100644 index 61bafaf31e..0000000000 --- a/docs/references/cdi/session/post-jwt.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create signed JWT -sidebar_label: post Create signed JWT -sidebar_position: 7 -description: 'POST `/appid-{appId}/recipe/jwt` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/post-session-refresh.mdx b/docs/references/cdi/session/post-session-refresh.mdx deleted file mode 100644 index cf0344924b..0000000000 --- a/docs/references/cdi/session/post-session-refresh.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Refresh session -sidebar_label: post Refresh session -sidebar_position: 8 -description: 'POST `/appid-{appId}/recipe/session/refresh` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/post-session-regenerate.mdx b/docs/references/cdi/session/post-session-regenerate.mdx deleted file mode 100644 index f9ac5e8920..0000000000 --- a/docs/references/cdi/session/post-session-regenerate.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Regenerate session -sidebar_label: post Regenerate session -sidebar_position: 9 -description: 'POST `/appid-{appId}/recipe/session/regenerate` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/post-session-remove.mdx b/docs/references/cdi/session/post-session-remove.mdx deleted file mode 100644 index 48fb235413..0000000000 --- a/docs/references/cdi/session/post-session-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Delete session -sidebar_label: post Delete session -sidebar_position: 6 -description: >- - POST /appid-{appId}/{tenantId}/recipe/session/remove endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/post-session-verify.mdx b/docs/references/cdi/session/post-session-verify.mdx deleted file mode 100644 index b463292a74..0000000000 --- a/docs/references/cdi/session/post-session-verify.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Verify session -sidebar_label: post Verify session -sidebar_position: 10 -description: 'POST `/appid-{appId}/recipe/session/verify` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/post-session.mdx b/docs/references/cdi/session/post-session.mdx deleted file mode 100644 index 2fce833b61..0000000000 --- a/docs/references/cdi/session/post-session.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create new session -sidebar_label: post Create new session -sidebar_position: 5 -description: 'POST `/appid-{appId}/{tenantId}/recipe/session` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/put-jwt-data.mdx b/docs/references/cdi/session/put-jwt-data.mdx deleted file mode 100644 index 6984e8aca7..0000000000 --- a/docs/references/cdi/session/put-jwt-data.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update JWT data -sidebar_label: put Update JWT data -sidebar_position: 11 -description: 'PUT `/appid-{appId}/{tenantId}/recipe/jwt/data` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/session/put-session-data.mdx b/docs/references/cdi/session/put-session-data.mdx deleted file mode 100644 index 365b8afe56..0000000000 --- a/docs/references/cdi/session/put-session-data.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update session data -sidebar_label: put Update session data -sidebar_position: 12 -description: 'PUT `/appid-{appId}/recipe/session/data` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/thirdparty/_category_.json b/docs/references/cdi/thirdparty/_category_.json deleted file mode 100644 index 1b293a57ec..0000000000 --- a/docs/references/cdi/thirdparty/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "ThirdParty", - "position": 12 -} diff --git a/docs/references/cdi/thirdparty/get-user.mdx b/docs/references/cdi/thirdparty/get-user.mdx deleted file mode 100644 index 32db0c12cd..0000000000 --- a/docs/references/cdi/thirdparty/get-user.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get third party user -sidebar_label: get Get third party user (deprecated) -sidebar_position: 1 -description: 'GET `/appid-{appId}/{tenantId}/recipe/user⠀` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/thirdparty/get-users-by-email.mdx b/docs/references/cdi/thirdparty/get-users-by-email.mdx deleted file mode 100644 index 7ad78bedf7..0000000000 --- a/docs/references/cdi/thirdparty/get-users-by-email.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get users by email -sidebar_label: get Get users by email (deprecated) -sidebar_position: 2 -description: >- - GET /appid-{appId}/{tenantId}/recipe/users/by-email endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/thirdparty/meta.ts b/docs/references/cdi/thirdparty/meta.ts new file mode 100644 index 0000000000..d7ece1d567 --- /dev/null +++ b/docs/references/cdi/thirdparty/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "ThirdParty", + icon: "share-2", + order: 12, +}); diff --git a/docs/references/cdi/thirdparty/post-signinup.mdx b/docs/references/cdi/thirdparty/post-signinup.mdx deleted file mode 100644 index 82ab8a7378..0000000000 --- a/docs/references/cdi/thirdparty/post-signinup.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in/up third party user -sidebar_label: post Sign in/up third party user -sidebar_position: 3 -description: 'POST `/appid-{appId}/{tenantId}/recipe/signinup` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-metadata/_category_.json b/docs/references/cdi/user-metadata/_category_.json deleted file mode 100644 index 3b50cac88d..0000000000 --- a/docs/references/cdi/user-metadata/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "User Metadata", - "position": 13 -} diff --git a/docs/references/cdi/user-metadata/get-user-metadata.mdx b/docs/references/cdi/user-metadata/get-user-metadata.mdx deleted file mode 100644 index 4823c3c5e8..0000000000 --- a/docs/references/cdi/user-metadata/get-user-metadata.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get user metadata -sidebar_label: get Get user metadata -sidebar_position: 1 -description: 'GET `/appid-{appId}/recipe/user/metadata` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-metadata/meta.ts b/docs/references/cdi/user-metadata/meta.ts new file mode 100644 index 0000000000..31f8b12aaf --- /dev/null +++ b/docs/references/cdi/user-metadata/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "User Metadata", + icon: "braces", + order: 13, +}); diff --git a/docs/references/cdi/user-metadata/post-user-metadata-remove.mdx b/docs/references/cdi/user-metadata/post-user-metadata-remove.mdx deleted file mode 100644 index bc8895ff69..0000000000 --- a/docs/references/cdi/user-metadata/post-user-metadata-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove user metadata -sidebar_label: post Remove user metadata -sidebar_position: 2 -description: >- - POST /appid-{appId}/recipe/user/metadata/remove endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-metadata/put-user-metadata.mdx b/docs/references/cdi/user-metadata/put-user-metadata.mdx deleted file mode 100644 index baf394c2c4..0000000000 --- a/docs/references/cdi/user-metadata/put-user-metadata.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Update user metadata -sidebar_label: put Update user metadata -sidebar_position: 3 -description: 'PUT `/appid-{appId}/recipe/user/metadata` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/_category_.json b/docs/references/cdi/user-roles/_category_.json deleted file mode 100644 index c2d65ca895..0000000000 --- a/docs/references/cdi/user-roles/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "User Roles", - "position": 14 -} diff --git a/docs/references/cdi/user-roles/get-permission-roles.mdx b/docs/references/cdi/user-roles/get-permission-roles.mdx deleted file mode 100644 index 5cc99fa88b..0000000000 --- a/docs/references/cdi/user-roles/get-permission-roles.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get permission roles -sidebar_label: get Get permission roles -sidebar_position: 3 -description: 'GET `/appid-{appId}/recipe/permission/roles` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/get-role-permissions.mdx b/docs/references/cdi/user-roles/get-role-permissions.mdx deleted file mode 100644 index 3fd85c7670..0000000000 --- a/docs/references/cdi/user-roles/get-role-permissions.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get role permissions -sidebar_label: get Get role permissions -sidebar_position: 4 -description: 'GET `/appid-{appId}/recipe/role/permissions` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/get-role-users.mdx b/docs/references/cdi/user-roles/get-role-users.mdx deleted file mode 100644 index a80bc2a241..0000000000 --- a/docs/references/cdi/user-roles/get-role-users.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get users with role -sidebar_label: get Get users with role -sidebar_position: 1 -description: >- - GET /appid-{appId}/{tenantId}/recipe/role/users endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/get-roles.mdx b/docs/references/cdi/user-roles/get-roles.mdx deleted file mode 100644 index 7f0681acca..0000000000 --- a/docs/references/cdi/user-roles/get-roles.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get all roles -sidebar_label: get Get all roles -sidebar_position: 5 -description: 'GET `/appid-{appId}/recipe/roles` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/get-user-roles.mdx b/docs/references/cdi/user-roles/get-user-roles.mdx deleted file mode 100644 index d5838f5a98..0000000000 --- a/docs/references/cdi/user-roles/get-user-roles.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get user roles -sidebar_label: get Get user roles -sidebar_position: 2 -description: >- - GET /appid-{appId}/{tenantId}/recipe/user/roles endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/meta.ts b/docs/references/cdi/user-roles/meta.ts new file mode 100644 index 0000000000..3d0e840006 --- /dev/null +++ b/docs/references/cdi/user-roles/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "User Roles", + icon: "shield-user", + order: 14, +}); diff --git a/docs/references/cdi/user-roles/post-role-permissions-remove.mdx b/docs/references/cdi/user-roles/post-role-permissions-remove.mdx deleted file mode 100644 index 45a65dfc53..0000000000 --- a/docs/references/cdi/user-roles/post-role-permissions-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove role permissions -sidebar_label: post Remove role permissions -sidebar_position: 7 -description: >- - POST /appid-{appId}/recipe/role/permissions/remove endpoint exposed by the CDI - API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/post-role-remove.mdx b/docs/references/cdi/user-roles/post-role-remove.mdx deleted file mode 100644 index 3036b00a87..0000000000 --- a/docs/references/cdi/user-roles/post-role-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Delete role -sidebar_label: post Delete role -sidebar_position: 8 -description: 'POST `/appid-{appId}/recipe/role/remove` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/post-user-role-remove.mdx b/docs/references/cdi/user-roles/post-user-role-remove.mdx deleted file mode 100644 index 694874c720..0000000000 --- a/docs/references/cdi/user-roles/post-user-role-remove.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove user role -sidebar_label: post Remove user role -sidebar_position: 6 -description: >- - POST /appid-{appId}/{tenantId}/recipe/user/role/remove endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/put-role.mdx b/docs/references/cdi/user-roles/put-role.mdx deleted file mode 100644 index 53f52bab4a..0000000000 --- a/docs/references/cdi/user-roles/put-role.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create or update role -sidebar_label: put Create or update role -sidebar_position: 10 -description: 'PUT `/appid-{appId}/recipe/role` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/user-roles/put-user-role.mdx b/docs/references/cdi/user-roles/put-user-role.mdx deleted file mode 100644 index 41bd2d3b82..0000000000 --- a/docs/references/cdi/user-roles/put-user-role.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Add user role -sidebar_label: put Add user role -sidebar_position: 9 -description: 'PUT `/appid-{appId}/{tenantId}/recipe/user/role` endpoint exposed by the CDI API' -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/_category_.json b/docs/references/cdi/webauthn/_category_.json deleted file mode 100644 index 14492eeddd..0000000000 --- a/docs/references/cdi/webauthn/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "WebAuthn", - "position": 15 -} diff --git a/docs/references/cdi/webauthn/delete-webauthn-credential.mdx b/docs/references/cdi/webauthn/delete-webauthn-credential.mdx deleted file mode 100644 index 493e002938..0000000000 --- a/docs/references/cdi/webauthn/delete-webauthn-credential.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove WebAuthn credential -sidebar_label: delete Remove WebAuthn credential -sidebar_position: 14 -description: >- - DELETE /appid-{appId}/{tenantId}/recipe/webauthn/user/credential/remove - endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/delete-webauthn-options.mdx b/docs/references/cdi/webauthn/delete-webauthn-options.mdx deleted file mode 100644 index fddcbc8c00..0000000000 --- a/docs/references/cdi/webauthn/delete-webauthn-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Remove WebAuthn options -sidebar_label: delete Remove WebAuthn options -sidebar_position: 13 -description: >- - DELETE /appid-{appId}/{tenantId}/recipe/webauthn/options/remove endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/get-webauthn-credential.mdx b/docs/references/cdi/webauthn/get-webauthn-credential.mdx deleted file mode 100644 index aabda2a243..0000000000 --- a/docs/references/cdi/webauthn/get-webauthn-credential.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get WebAuthn credential -sidebar_label: get Get WebAuthn credential -sidebar_position: 2 -description: >- - GET /appid-{appId}/{tenantId}/recipe/webauthn/user/credential endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/get-webauthn-credentials.mdx b/docs/references/cdi/webauthn/get-webauthn-credentials.mdx deleted file mode 100644 index 2ab0db8c68..0000000000 --- a/docs/references/cdi/webauthn/get-webauthn-credentials.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: List WebAuthn credentials -sidebar_label: get List WebAuthn credentials -sidebar_position: 3 -description: >- - GET /appid-{appId}/{tenantId}/recipe/webauthn/user/credential/list endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/get-webauthn-options.mdx b/docs/references/cdi/webauthn/get-webauthn-options.mdx deleted file mode 100644 index 0e85f2cf5d..0000000000 --- a/docs/references/cdi/webauthn/get-webauthn-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get WebAuthn options -sidebar_label: get Get WebAuthn options -sidebar_position: 1 -description: >- - GET /appid-{appId}/{tenantId}/recipe/webauthn/options endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/meta.ts b/docs/references/cdi/webauthn/meta.ts new file mode 100644 index 0000000000..b97f9cec2f --- /dev/null +++ b/docs/references/cdi/webauthn/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "WebAuthn", + icon: "fingerprint", + order: 15, +}); diff --git a/docs/references/cdi/webauthn/post-webauthn-recover.mdx b/docs/references/cdi/webauthn/post-webauthn-recover.mdx deleted file mode 100644 index 882b7a73ee..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-recover.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Recover WebAuthn user -sidebar_label: get Recover WebAuthn user -sidebar_position: 4 -description: >- - GET /appid-{appId}/{tenantId}/recipe/webauthn/user/recover endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-register.mdx b/docs/references/cdi/webauthn/post-webauthn-register.mdx deleted file mode 100644 index 19ab7996f7..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-register.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Register WebAuthn credential -sidebar_label: post Register WebAuthn credential -sidebar_position: 9 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/user/credential/register - endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-registration-options.mdx b/docs/references/cdi/webauthn/post-webauthn-registration-options.mdx deleted file mode 100644 index 0716659d87..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-registration-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate registration options -sidebar_label: post Generate registration options -sidebar_position: 5 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/options/register endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-signin-options.mdx b/docs/references/cdi/webauthn/post-webauthn-signin-options.mdx deleted file mode 100644 index 0c952a15b5..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-signin-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate authentication options -sidebar_label: post Generate authentication options -sidebar_position: 6 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/options/signin endpoint exposed - by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-signin.mdx b/docs/references/cdi/webauthn/post-webauthn-signin.mdx deleted file mode 100644 index d92ccd8124..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-signin.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Sign in WebAuthn user -sidebar_label: post Sign in WebAuthn user -sidebar_position: 7 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/signin endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-signup.mdx b/docs/references/cdi/webauthn/post-webauthn-signup.mdx deleted file mode 100644 index e908dc8789..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-signup.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Sign up WebAuthn user -sidebar_label: post Sign up WebAuthn user -sidebar_position: 8 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/signup endpoint exposed by the - CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-token-consume.mdx b/docs/references/cdi/webauthn/post-webauthn-token-consume.mdx deleted file mode 100644 index 49ddadc3fd..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-token-consume.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Consume recovery token -sidebar_label: post Consume recovery token -sidebar_position: 11 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/user/recover/token/consume - endpoint exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/post-webauthn-token-recovery.mdx b/docs/references/cdi/webauthn/post-webauthn-token-recovery.mdx deleted file mode 100644 index 70703160e4..0000000000 --- a/docs/references/cdi/webauthn/post-webauthn-token-recovery.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate recovery token -sidebar_label: post Generate recovery token -sidebar_position: 10 -description: >- - POST /appid-{appId}/{tenantId}/recipe/webauthn/user/recover/token endpoint - exposed by the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/cdi/webauthn/put-webauthn-email.mdx b/docs/references/cdi/webauthn/put-webauthn-email.mdx deleted file mode 100644 index 40233be857..0000000000 --- a/docs/references/cdi/webauthn/put-webauthn-email.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Update email -sidebar_label: put Update email -sidebar_position: 12 -description: >- - PUT /appid-{appId}/{tenantId}/recipe/webauthn/user/email endpoint exposed by - the CDI API -page_type: api-reference -hide_title: true -category: cdi ---- - - diff --git a/docs/references/compatibility-table.mdx b/docs/references/compatibility-table.mdx deleted file mode 100644 index 7219c8e6cd..0000000000 --- a/docs/references/compatibility-table.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: SDK Compatibility Table -hide_title: true -hide_table_of_contents: true -sidebar_position: 9 -pagination_next: null -pagination_prev: null -description: Find the compatible SDK/Core version for your specific tech stack. -page_type: overview -category: references ---- - - -# SDK compatibility table - - -This page helps you find the right version of an SDK/Core given your tech stack. -If you are starting out, **you do not need to use this page**. -Instead, you can use the [Quickstart](/docs/quickstart/introduction) to get started. - - diff --git a/docs/references/fdi/_category_.json b/docs/references/fdi/_category_.json deleted file mode 100644 index c175e4b97d..0000000000 --- a/docs/references/fdi/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "Frontend Driver Interface", - "customProps": { "isMainCategory": true }, - "position": 4 -} diff --git a/docs/references/fdi/email-password/_category_.json b/docs/references/fdi/email-password/_category_.json deleted file mode 100644 index 11510dbb2c..0000000000 --- a/docs/references/fdi/email-password/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Email/Password", - "position": 2 -} diff --git a/docs/references/fdi/email-password/get-emailpassword-email-exists.mdx b/docs/references/fdi/email-password/get-emailpassword-email-exists.mdx deleted file mode 100644 index 40a92e4bd6..0000000000 --- a/docs/references/fdi/email-password/get-emailpassword-email-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check email exists -sidebar_label: get Check email exists -sidebar_position: 1 -description: >- - GET /{apiBasePath}/{tenantId}/emailpassword/email/exists endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-password/get-signup-email-exists.mdx b/docs/references/fdi/email-password/get-signup-email-exists.mdx deleted file mode 100644 index e6e956c88d..0000000000 --- a/docs/references/fdi/email-password/get-signup-email-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check email exists (deprecated) -sidebar_label: get Check email exists (deprecated) (deprecated) -sidebar_position: 2 -description: >- - GET /{apiBasePath}/{tenantId}/signup/email/exists endpoint exposed by the FDI - API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-password/meta.ts b/docs/references/fdi/email-password/meta.ts new file mode 100644 index 0000000000..a71c1226e2 --- /dev/null +++ b/docs/references/fdi/email-password/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email/Password", + icon: "rectangle-ellipsis", + order: 2, +}); diff --git a/docs/references/fdi/email-password/post-signin.mdx b/docs/references/fdi/email-password/post-signin.mdx deleted file mode 100644 index fe6bc0b2b6..0000000000 --- a/docs/references/fdi/email-password/post-signin.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in with email -sidebar_label: post Sign in with email -sidebar_position: 3 -description: 'POST `/{apiBasePath}/{tenantId}/signin` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-password/post-signup.mdx b/docs/references/fdi/email-password/post-signup.mdx deleted file mode 100644 index 9f281315be..0000000000 --- a/docs/references/fdi/email-password/post-signup.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign up with email -sidebar_label: post Sign up with email -sidebar_position: 4 -description: 'POST `/{apiBasePath}/{tenantId}/signup` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-password/post-user-password-reset-token.mdx b/docs/references/fdi/email-password/post-user-password-reset-token.mdx deleted file mode 100644 index 442063c908..0000000000 --- a/docs/references/fdi/email-password/post-user-password-reset-token.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate password reset token -sidebar_label: post Generate password reset token -sidebar_position: 6 -description: >- - POST /{apiBasePath}/{tenantId}/user/password/reset/token endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-password/post-user-password-reset.mdx b/docs/references/fdi/email-password/post-user-password-reset.mdx deleted file mode 100644 index 99446dbfcd..0000000000 --- a/docs/references/fdi/email-password/post-user-password-reset.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Reset user password -sidebar_label: post Reset user password -sidebar_position: 5 -description: >- - POST /{apiBasePath}/{tenantId}/user/password/reset endpoint exposed by the FDI - API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-verification/_category_.json b/docs/references/fdi/email-verification/_category_.json deleted file mode 100644 index 9ce848b736..0000000000 --- a/docs/references/fdi/email-verification/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Email Verification", - "position": 1 -} diff --git a/docs/references/fdi/email-verification/get-user-email-verify.mdx b/docs/references/fdi/email-verification/get-user-email-verify.mdx deleted file mode 100644 index ed12b68fb0..0000000000 --- a/docs/references/fdi/email-verification/get-user-email-verify.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Check email verification status -sidebar_label: get Check email verification status -sidebar_position: 1 -description: 'GET `/{apiBasePath}/user/email/verify` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-verification/meta.ts b/docs/references/fdi/email-verification/meta.ts new file mode 100644 index 0000000000..4e01897ade --- /dev/null +++ b/docs/references/fdi/email-verification/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Email Verification", + icon: "mail-check", + order: 1, +}); diff --git a/docs/references/fdi/email-verification/post-user-email-verify-token.mdx b/docs/references/fdi/email-verification/post-user-email-verify-token.mdx deleted file mode 100644 index 6ffb642725..0000000000 --- a/docs/references/fdi/email-verification/post-user-email-verify-token.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Send email verification -sidebar_label: post Send email verification -sidebar_position: 3 -description: >- - POST /{apiBasePath}/{tenantId}/user/email/verify/token endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/email-verification/post-user-email-verify.mdx b/docs/references/fdi/email-verification/post-user-email-verify.mdx deleted file mode 100644 index ebd459825c..0000000000 --- a/docs/references/fdi/email-verification/post-user-email-verify.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Verify email address -sidebar_label: post Verify email address -sidebar_position: 2 -description: >- - POST /{apiBasePath}/{tenantId}/user/email/verify endpoint exposed by the FDI - API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/get-example.mdx b/docs/references/fdi/get-example.mdx deleted file mode 100644 index 7b5f75b367..0000000000 --- a/docs/references/fdi/get-example.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Test authentication -sidebar_label: get Test authentication -sidebar_position: 9.5 -description: GET `/example` endpoint exposed by the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/get-jwt-jwks-json.mdx b/docs/references/fdi/get-jwt-jwks-json.mdx deleted file mode 100644 index c9f11ccf78..0000000000 --- a/docs/references/fdi/get-jwt-jwks-json.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get JWT keys -sidebar_label: get Get JWT keys -sidebar_position: 9.2 -description: 'GET `/{apiBasePath}/jwt/jwks.json` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/get-loginmethods.mdx b/docs/references/fdi/get-loginmethods.mdx deleted file mode 100644 index d2b412ed70..0000000000 --- a/docs/references/fdi/get-loginmethods.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get enabled login methods -sidebar_label: get Get enabled login methods -sidebar_position: 9.1 -description: 'GET `/{apiBasePath}/{tenantId}/loginmethods` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/get-well-known-openid-configuration.mdx b/docs/references/fdi/get-well-known-openid-configuration.mdx deleted file mode 100644 index 2e989c61d7..0000000000 --- a/docs/references/fdi/get-well-known-openid-configuration.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get OpenID config -sidebar_label: get Get OpenID config -sidebar_position: 9.4 -description: >- - GET /{apiBasePath}/.well-known/openid-configuration endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/introduction.mdx b/docs/references/fdi/introduction.mdx index 8991988d39..def57bbe7b 100644 --- a/docs/references/fdi/introduction.mdx +++ b/docs/references/fdi/introduction.mdx @@ -1,26 +1,17 @@ --- title: Introduction -hide_title: true -sidebar_position: 0.1 -description: >- - Access detailed API specifications for integrating with SuperTokens Core and - frontend SDKs. -pagination_prev: null -pagination_next: null -page_type: overview -category: fdi +description: Access detailed API specifications for integrating with SuperTokens Core and frontend SDKs. +sidebar: + order: 0.1 --- -# Frontend Driver Interface - - ## Overview The **FDI**, Frontend Driver Interface, is the API exposed by the **SuperTokens Backend SDKs**. It is meant to be consumed only by your frontend applications. -:::info no-title -In most cases, you don't need to directly interact with the API, since the existing [frontend SDKS](/docs/references/backend-sdks/reference) are built on top of it. +:::info +In most cases, you don't need to directly interact with the API, since the existing [frontend SDKS](/references/backend-sdks/reference) are built on top of it. If you are using something that does not have SDK support, then you can make use of the FDI resources. ::: @@ -28,11 +19,11 @@ If you are using something that does not have SDK support, then you can make use ### URL Structure All the endpoints are prefixed with `{apiBasePath}`. -This is the property with the same name that you set during [backend SDK initialization](/docs/quickstart/backend-setup#2-initialize-the-sdk). +This is the property with the same name that you set during [backend SDK initialization](/quickstart#22-initialize-the-backend-sdk). Tenant specific actions include a `{tenantId}` parameter. If not set, the default tenant will be used. -Given the following endpoint: `/{apiBasePath}/{tenantId}/signinup`, and the `apiBasePath` set to `auth` during [initialization](/docs/quickstart/backend-setup#2-initialize-the-sdk): +Given the following endpoint: `/{apiBasePath}/{tenantId}/signinup`, and the `apiBasePath` set to `auth` during [initialization](/quickstart#22-initialize-the-backend-sdk): - You can call it without the actual parameter, using `/auth/signinup` in your action. - You can set the values to target a specific tenant: `/auth/myTenant/signinup`. @@ -53,6 +44,4 @@ To know which version you should see: Since this API allows users to sign up or login, most of the endpoints do not require any type of credentials. However, in some situations you have to authenticate your requests. In those cases, you need to choose between using cookies or headers. -The method that you have to use depends on the [token transfer method](/docs/post-authentication/session-management/switch-between-cookies-and-header-authentication) used in your application. - - +The method that you have to use depends on the [token transfer method](/post-authentication/session-management/switch-between-cookies-and-header-authentication) used in your application. diff --git a/docs/references/fdi/meta.ts b/docs/references/fdi/meta.ts new file mode 100644 index 0000000000..46acc31588 --- /dev/null +++ b/docs/references/fdi/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Frontend Driver Interface", + icon: "file-code-2", + order: 60, +}); diff --git a/docs/references/fdi/mfa/_category_.json b/docs/references/fdi/mfa/_category_.json deleted file mode 100644 index 0b63ac877d..0000000000 --- a/docs/references/fdi/mfa/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "MFA", - "position": 3 -} diff --git a/docs/references/fdi/mfa/get-totp-device-list.mdx b/docs/references/fdi/mfa/get-totp-device-list.mdx deleted file mode 100644 index 6c169870a2..0000000000 --- a/docs/references/fdi/mfa/get-totp-device-list.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: List TOTP devices -sidebar_label: get List TOTP devices -sidebar_position: 1 -description: 'GET `/{apiBasePath}/totp/device/list` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/mfa/meta.ts b/docs/references/fdi/mfa/meta.ts new file mode 100644 index 0000000000..a3e7d0ef7d --- /dev/null +++ b/docs/references/fdi/mfa/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "MFA", + icon: "shield-check", + order: 3, +}); diff --git a/docs/references/fdi/mfa/post-totp-device-remove.mdx b/docs/references/fdi/mfa/post-totp-device-remove.mdx deleted file mode 100644 index d56df888a0..0000000000 --- a/docs/references/fdi/mfa/post-totp-device-remove.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Remove TOTP device -sidebar_label: post Remove TOTP device -sidebar_position: 3 -description: 'POST `/{apiBasePath}/totp/device/remove` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/mfa/post-totp-device-verify.mdx b/docs/references/fdi/mfa/post-totp-device-verify.mdx deleted file mode 100644 index 45567b0abf..0000000000 --- a/docs/references/fdi/mfa/post-totp-device-verify.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Verify TOTP device -sidebar_label: post Verify TOTP device -sidebar_position: 4 -description: 'POST `/{apiBasePath}/totp/device/verify` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/mfa/post-totp-device.mdx b/docs/references/fdi/mfa/post-totp-device.mdx deleted file mode 100644 index f068fccfa0..0000000000 --- a/docs/references/fdi/mfa/post-totp-device.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Create TOTP device -sidebar_label: post Create TOTP device -sidebar_position: 2 -description: 'POST `/{apiBasePath}/totp/device` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/mfa/post-totp-verify.mdx b/docs/references/fdi/mfa/post-totp-verify.mdx deleted file mode 100644 index 1f15527a7e..0000000000 --- a/docs/references/fdi/mfa/post-totp-verify.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Verify TOTP code -sidebar_label: post Verify TOTP code -sidebar_position: 5 -description: 'POST `/{apiBasePath}/totp/verify` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/mfa/put-mfa-info.mdx b/docs/references/fdi/mfa/put-mfa-info.mdx deleted file mode 100644 index 2e5e5ef949..0000000000 --- a/docs/references/fdi/mfa/put-mfa-info.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get MFA factors information -sidebar_label: put Get MFA factors information -sidebar_position: 6 -description: 'PUT `/{apiBasePath}/mfa/info` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/_category_.json b/docs/references/fdi/oauth/_category_.json deleted file mode 100644 index 220c6d8ae0..0000000000 --- a/docs/references/fdi/oauth/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "OAuth", - "position": 4 -} diff --git a/docs/references/fdi/oauth/get-oauth-auth.mdx b/docs/references/fdi/oauth/get-oauth-auth.mdx deleted file mode 100644 index b566b11c25..0000000000 --- a/docs/references/fdi/oauth/get-oauth-auth.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Start OAuth login -sidebar_label: get Start OAuth login -sidebar_position: 1 -description: 'GET `/{apiBasePath}/oauth/auth` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/get-oauth-end_session.mdx b/docs/references/fdi/oauth/get-oauth-end_session.mdx deleted file mode 100644 index 6246a2c00d..0000000000 --- a/docs/references/fdi/oauth/get-oauth-end_session.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: End session redirect -sidebar_label: get End session redirect -sidebar_position: 2 -description: 'GET `/{apiBasePath}/oauth/end_session` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/get-oauth-login.mdx b/docs/references/fdi/oauth/get-oauth-login.mdx deleted file mode 100644 index 0cfde09ab5..0000000000 --- a/docs/references/fdi/oauth/get-oauth-login.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Continue OAuth login -sidebar_label: get Continue OAuth login -sidebar_position: 3 -description: 'GET `/{apiBasePath}/oauth/login` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/get-oauth-logininfo.mdx b/docs/references/fdi/oauth/get-oauth-logininfo.mdx deleted file mode 100644 index dd7f78c766..0000000000 --- a/docs/references/fdi/oauth/get-oauth-logininfo.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get OAuth login info -sidebar_label: get Get OAuth login info -sidebar_position: 4 -description: 'GET `/{apiBasePath}/oauth/logininfo` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/get-oauth-userinfo.mdx b/docs/references/fdi/oauth/get-oauth-userinfo.mdx deleted file mode 100644 index c4f7d9ef73..0000000000 --- a/docs/references/fdi/oauth/get-oauth-userinfo.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get OAuth user info -sidebar_label: get Get OAuth user info -sidebar_position: 5 -description: 'GET `/{apiBasePath}/oauth/userinfo` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/meta.ts b/docs/references/fdi/oauth/meta.ts new file mode 100644 index 0000000000..390c045486 --- /dev/null +++ b/docs/references/fdi/oauth/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "OAuth", + icon: "key-round", + order: 4, +}); diff --git a/docs/references/fdi/oauth/post-oauth-end_session.mdx b/docs/references/fdi/oauth/post-oauth-end_session.mdx deleted file mode 100644 index f8cdbe0d78..0000000000 --- a/docs/references/fdi/oauth/post-oauth-end_session.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: End OAuth session -sidebar_label: post End OAuth session -sidebar_position: 6 -description: 'POST `/{apiBasePath}/oauth/end_session` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/post-oauth-introspect.mdx b/docs/references/fdi/oauth/post-oauth-introspect.mdx deleted file mode 100644 index 5da6c7b16e..0000000000 --- a/docs/references/fdi/oauth/post-oauth-introspect.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Introspect OAuth token -sidebar_label: post Introspect OAuth token -sidebar_position: 7 -description: 'POST `/{apiBasePath}/oauth/introspect` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/post-oauth-logout.mdx b/docs/references/fdi/oauth/post-oauth-logout.mdx deleted file mode 100644 index 3b9cac51de..0000000000 --- a/docs/references/fdi/oauth/post-oauth-logout.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Logout OAuth user -sidebar_label: post Logout OAuth user -sidebar_position: 8 -description: 'POST `/{apiBasePath}/oauth/logout` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/post-oauth-revoke.mdx b/docs/references/fdi/oauth/post-oauth-revoke.mdx deleted file mode 100644 index 66d51f129a..0000000000 --- a/docs/references/fdi/oauth/post-oauth-revoke.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Revoke OAuth token -sidebar_label: post Revoke OAuth token -sidebar_position: 9 -description: 'POST `/{apiBasePath}/oauth/revoke` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/oauth/post-oauth-token.mdx b/docs/references/fdi/oauth/post-oauth-token.mdx deleted file mode 100644 index 607f1a614f..0000000000 --- a/docs/references/fdi/oauth/post-oauth-token.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Exchange OAuth grant -sidebar_label: post Exchange OAuth grant -sidebar_position: 10 -description: 'POST `/{apiBasePath}/oauth/token` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/_category_.json b/docs/references/fdi/passwordless/_category_.json deleted file mode 100644 index 9d8d96266f..0000000000 --- a/docs/references/fdi/passwordless/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Passwordless", - "position": 5 -} diff --git a/docs/references/fdi/passwordless/get-passwordless-email-exists.mdx b/docs/references/fdi/passwordless/get-passwordless-email-exists.mdx deleted file mode 100644 index 78a8adc489..0000000000 --- a/docs/references/fdi/passwordless/get-passwordless-email-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check email exists -sidebar_label: get Check email exists -sidebar_position: 1 -description: >- - GET /{apiBasePath}/{tenantId}/passwordless/email/exists endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/get-passwordless-phoneNumber-exists.mdx b/docs/references/fdi/passwordless/get-passwordless-phoneNumber-exists.mdx deleted file mode 100644 index 9156475538..0000000000 --- a/docs/references/fdi/passwordless/get-passwordless-phoneNumber-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check phone exists -sidebar_label: get Check phone exists -sidebar_position: 2 -description: >- - GET /{apiBasePath}/{tenantId}/passwordless/phoneNumber/exists endpoint exposed - by the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/get-signup-email-exists.mdx b/docs/references/fdi/passwordless/get-signup-email-exists.mdx deleted file mode 100644 index 2c1d61fb79..0000000000 --- a/docs/references/fdi/passwordless/get-signup-email-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check email exists (deprecated) -sidebar_label: get Check email exists (deprecated) (deprecated) -sidebar_position: 3 -description: >- - GET /{apiBasePath}/{tenantId}/signup/email/exists⠀⠀ endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/get-signup-phoneNumber-exists.mdx b/docs/references/fdi/passwordless/get-signup-phoneNumber-exists.mdx deleted file mode 100644 index 5629b3366f..0000000000 --- a/docs/references/fdi/passwordless/get-signup-phoneNumber-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check phone exists (deprecated) -sidebar_label: get Check phone exists (deprecated) (deprecated) -sidebar_position: 4 -description: >- - GET /{apiBasePath}/{tenantId}/signup/phoneNumber/exists endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/meta.ts b/docs/references/fdi/passwordless/meta.ts new file mode 100644 index 0000000000..eee8e77fef --- /dev/null +++ b/docs/references/fdi/passwordless/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Passwordless", + icon: "wand-sparkles", + order: 5, +}); diff --git a/docs/references/fdi/passwordless/post-signinup-code-consume.mdx b/docs/references/fdi/passwordless/post-signinup-code-consume.mdx deleted file mode 100644 index be106e0ea0..0000000000 --- a/docs/references/fdi/passwordless/post-signinup-code-consume.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Complete passwordless sign in/up -sidebar_label: post Complete passwordless sign in/up -sidebar_position: 6 -description: >- - POST /{apiBasePath}/{tenantId}/signinup/code/consume endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/post-signinup-code-resend.mdx b/docs/references/fdi/passwordless/post-signinup-code-resend.mdx deleted file mode 100644 index 0edb47ecc2..0000000000 --- a/docs/references/fdi/passwordless/post-signinup-code-resend.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Resend passwordless code -sidebar_label: post Resend passwordless code -sidebar_position: 7 -description: >- - POST /{apiBasePath}/{tenantId}/signinup/code/resend endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/passwordless/post-signinup-code.mdx b/docs/references/fdi/passwordless/post-signinup-code.mdx deleted file mode 100644 index b10185e276..0000000000 --- a/docs/references/fdi/passwordless/post-signinup-code.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Start passwordless sign in/up -sidebar_label: post Start passwordless sign in/up -sidebar_position: 5 -description: 'POST `/{apiBasePath}/{tenantId}/signinup/code` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/session/_category_.json b/docs/references/fdi/session/_category_.json deleted file mode 100644 index 17d5425ed0..0000000000 --- a/docs/references/fdi/session/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Session", - "position": 6 -} diff --git a/docs/references/fdi/session/meta.ts b/docs/references/fdi/session/meta.ts new file mode 100644 index 0000000000..bf447910b3 --- /dev/null +++ b/docs/references/fdi/session/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Session", + icon: "clock-3", + order: 6, +}); diff --git a/docs/references/fdi/session/post-session-refresh.mdx b/docs/references/fdi/session/post-session-refresh.mdx deleted file mode 100644 index d63cfe24a7..0000000000 --- a/docs/references/fdi/session/post-session-refresh.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Refresh user session -sidebar_label: post Refresh user session -sidebar_position: 1 -description: 'POST `/{apiBasePath}/session/refresh` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/session/post-signout.mdx b/docs/references/fdi/session/post-signout.mdx deleted file mode 100644 index 9f70e1043e..0000000000 --- a/docs/references/fdi/session/post-signout.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign out user -sidebar_label: post Sign out user -sidebar_position: 2 -description: 'POST `/{apiBasePath}/signout` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/thirdparty/_category_.json b/docs/references/fdi/thirdparty/_category_.json deleted file mode 100644 index 50735dc4ae..0000000000 --- a/docs/references/fdi/thirdparty/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "ThirdParty", - "position": 7 -} diff --git a/docs/references/fdi/thirdparty/get-authorisationurl.mdx b/docs/references/fdi/thirdparty/get-authorisationurl.mdx deleted file mode 100644 index 65d334f143..0000000000 --- a/docs/references/fdi/thirdparty/get-authorisationurl.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Get third party auth URL -sidebar_label: get Get third party auth URL -sidebar_position: 1 -description: 'GET `/{apiBasePath}/{tenantId}/authorisationurl` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/thirdparty/meta.ts b/docs/references/fdi/thirdparty/meta.ts new file mode 100644 index 0000000000..431441a177 --- /dev/null +++ b/docs/references/fdi/thirdparty/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "ThirdParty", + icon: "share-2", + order: 7, +}); diff --git a/docs/references/fdi/thirdparty/post-callback-apple.mdx b/docs/references/fdi/thirdparty/post-callback-apple.mdx deleted file mode 100644 index 3154845695..0000000000 --- a/docs/references/fdi/thirdparty/post-callback-apple.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Handle Apple sign in -sidebar_label: post Handle Apple sign in -sidebar_position: 3 -description: 'POST `/{apiBasePath}/callback/apple` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/thirdparty/post-signinup.mdx b/docs/references/fdi/thirdparty/post-signinup.mdx deleted file mode 100644 index 99fb6d3403..0000000000 --- a/docs/references/fdi/thirdparty/post-signinup.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in/up with third party -sidebar_label: post Sign in/up with third party -sidebar_position: 2 -description: 'POST `/{apiBasePath}/{tenantId}/signinup` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/_category_.json b/docs/references/fdi/webauthn/_category_.json deleted file mode 100644 index bc41f2b916..0000000000 --- a/docs/references/fdi/webauthn/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "WebAuthn", - "position": 8 -} diff --git a/docs/references/fdi/webauthn/get-webauthn-email-exists.mdx b/docs/references/fdi/webauthn/get-webauthn-email-exists.mdx deleted file mode 100644 index 6ebf26ceab..0000000000 --- a/docs/references/fdi/webauthn/get-webauthn-email-exists.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Check WebAuthn email exists -sidebar_label: get Check WebAuthn email exists -sidebar_position: 1 -description: >- - GET /{apiBasePath}/{tenantId}/webauthn/email/exists endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/meta.ts b/docs/references/fdi/webauthn/meta.ts new file mode 100644 index 0000000000..9eba6c4aa2 --- /dev/null +++ b/docs/references/fdi/webauthn/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "WebAuthn", + icon: "fingerprint", + order: 8, +}); diff --git a/docs/references/fdi/webauthn/post-webauthn-credential.mdx b/docs/references/fdi/webauthn/post-webauthn-credential.mdx deleted file mode 100644 index 88c987bf9b..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-credential.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Register WebAuthn credential -sidebar_label: post Register WebAuthn credential -sidebar_position: 2 -description: >- - POST /{apiBasePath}/{tenantId}/webauthn/credential endpoint exposed by the FDI - API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-recover-account-token.mdx b/docs/references/fdi/webauthn/post-webauthn-recover-account-token.mdx deleted file mode 100644 index c7b99f8499..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-recover-account-token.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Generate WebAuthn recovery token -sidebar_label: post Generate WebAuthn recovery token -sidebar_position: 4 -description: >- - POST /{apiBasePath}/{tenantId}/webauthn/recover/account/token endpoint exposed - by the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-recover-account.mdx b/docs/references/fdi/webauthn/post-webauthn-recover-account.mdx deleted file mode 100644 index b93f126e47..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-recover-account.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Recover WebAuthn account -sidebar_label: post Recover WebAuthn account -sidebar_position: 3 -description: >- - POST /{apiBasePath}/{tenantId}/webauthn/recover/account endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-register-options.mdx b/docs/references/fdi/webauthn/post-webauthn-register-options.mdx deleted file mode 100644 index 4f59ed4ca4..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-register-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get WebAuthn registration options -sidebar_label: post Get WebAuthn registration options -sidebar_position: 5 -description: >- - POST /{apiBasePath}/{tenantId}/webauthn/register/options endpoint exposed by - the FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-signin-options.mdx b/docs/references/fdi/webauthn/post-webauthn-signin-options.mdx deleted file mode 100644 index 432f34c02d..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-signin-options.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Get WebAuthn sign in options -sidebar_label: post Get WebAuthn sign in options -sidebar_position: 7 -description: >- - POST /{apiBasePath}/{tenantId}/webauthn/signin/options endpoint exposed by the - FDI API -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-signin.mdx b/docs/references/fdi/webauthn/post-webauthn-signin.mdx deleted file mode 100644 index 87258ce474..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-signin.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign in with WebAuthn -sidebar_label: post Sign in with WebAuthn -sidebar_position: 6 -description: 'POST `/{apiBasePath}/{tenantId}/webauthn/signin` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/fdi/webauthn/post-webauthn-signup.mdx b/docs/references/fdi/webauthn/post-webauthn-signup.mdx deleted file mode 100644 index 5c895b4ab2..0000000000 --- a/docs/references/fdi/webauthn/post-webauthn-signup.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Sign up with WebAuthn -sidebar_label: post Sign up with WebAuthn -sidebar_position: 8 -description: 'POST `/{apiBasePath}/{tenantId}/webauthn/signup` endpoint exposed by the FDI API' -page_type: api-reference -hide_title: true -category: fdi ---- - - diff --git a/docs/references/frontend-sdks/_category_.json b/docs/references/frontend-sdks/_category_.json deleted file mode 100644 index c9e8fa71c9..0000000000 --- a/docs/references/frontend-sdks/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Frontend SDKs", - "customProps": { - "isMainCategory": true - }, - "position": 3 -} diff --git a/docs/references/frontend-sdks/function-overrides.mdx b/docs/references/frontend-sdks/function-overrides.mdx index cc8b3e1fb7..b2f92e9e19 100644 --- a/docs/references/frontend-sdks/function-overrides.mdx +++ b/docs/references/frontend-sdks/function-overrides.mdx @@ -1,15 +1,11 @@ --- title: Function Overrides -hide_title: true -sidebar_position: 2 description: Override frontend authentication functions to implement custom use cases. -page_type: sdk-reference -category: frontend-sdk +sidebar: + icon: code + order: 20 --- - -# Function overrides - ## Overview **Function overrides** let you customize the behavior of the functions used internally, by the SDKs. @@ -32,15 +28,20 @@ If you are calling the backend SDK endpoints directly this code does not run in The code snippet shows the general flow of overriding a function. You inject your own custom logic while also calling the original implementation of the function. -:::info -The next examples include a couple of override samples. -See all the [functions that can be overridden here](https://supertokens.com/docs/auth-react/modules/recipe_thirdpartyemailpassword.html#RecipeInterface) +:::info[The next examples include a couple of override samples.] +See all the [functions that can be overridden here](https://supertokens.com/docs/references/frontend-sdks/function-overrides) ::: - - - + + +:::info +See all the [functions that can be overridden here](https://supertokens.com/docs/references/frontend-sdks/function-overrides) +::: + + + + ```tsx import SuperTokens from "supertokens-auth-react"; import Session from "supertokens-auth-react/recipe/session"; @@ -48,169 +49,145 @@ import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - Session.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding the function for checking - // if a session exists - doesSessionExist: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.doesSessionExist(input); - } - } - } - } - // highlight-end - }), - EmailPassword.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding what happens when a user - // clicks the sign up button. - signUp: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.signUp(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }), - ThirdParty.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding what happens when a user - // clicks the sign in or sign up button. - signInAndUp: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.signInAndUp(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + Session.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding the function for checking + // if a session exists + doesSessionExist: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.doesSessionExist(input); + }, + }; + }, + }, + }), + EmailPassword.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding what happens when a user + // clicks the sign up button. + signUp: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.signUp(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + ThirdParty.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding what happens when a user + // clicks the sign in or sign up button. + signInAndUp: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.signInAndUp(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + ], }); ``` - - - - -:::info -See all the [functions that can be overridden here](https://supertokens.com/docs/auth-react/modules/recipe_thirdpartyemailpassword.html#RecipeInterface) -::: - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - recipeList: [ - supertokensUISession.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding the function for checking - // if a session exists - doesSessionExist: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.doesSessionExist(input); - } - } - } - } - // highlight-end - }), - supertokensUIEmailPassword.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding what happens when a user - // clicks the sign up button. - signUp: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.signUp(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }), - supertokensUIThirdParty.init({ - // highlight-start - override: { - functions: (originalImplementation) => { - return { - ...originalImplementation, - - // we will only be overriding what happens when a user - // clicks the sign in or sign up button. - signInAndUp: async function (input) { - // TODO: some custom logic - - // or call the default behaviour as show below - return originalImplementation.signInAndUp(input); - }, - // ... - // TODO: override more functions - } - } - } - // highlight-end - }) - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUISession.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding the function for checking + // if a session exists + doesSessionExist: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.doesSessionExist(input); + }, + }; + }, + }, + }), + supertokensUIEmailPassword.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding what happens when a user + // clicks the sign up button. + signUp: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.signUp(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + supertokensUIThirdParty.init({ + override: { + functions: (originalImplementation) => { + return { + ...originalImplementation, + + // we will only be overriding what happens when a user + // clicks the sign in or sign up button. + signInAndUp: async function (input) { + // TODO: some custom logic + + // or call the default behaviour as show below + return originalImplementation.signInAndUp(input); + }, + // ... + // TODO: override more functions + }; + }, + }, + }), + ], }); ``` - - - - + + diff --git a/docs/references/frontend-sdks/hooks.mdx b/docs/references/frontend-sdks/hooks.mdx index 728a9fb064..eb61cd60af 100644 --- a/docs/references/frontend-sdks/hooks.mdx +++ b/docs/references/frontend-sdks/hooks.mdx @@ -1,140 +1,124 @@ --- title: Hooks -hide_title: true -sidebar_position: 3 -description: >- - Handle user actions for logging, analytics, or side effects using the Handle - Event Hook. -page_type: sdk-reference -category: frontend-sdk +description: Handle user actions for logging, analytics, or side effects using the Handle Event Hook. +sidebar: + icon: webhook + order: 30 --- - -# Hooks - ## Overview Hooks are a way to trigger custom logic when certain actions happen in the authentication process. - + --- ## Handle event hook - + Each frontend recipe emits events when certain actions happen. You can use this hook to trigger side effects when something happens in the authentication process. This can address things like logging or analytics. - - - + + ```tsx import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; EmailPassword.init({ - onHandleEvent: (context) => { - if (context.action === "PASSWORD_RESET_SUCCESSFUL") { - - } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { - - } else if (context.action === "SUCCESS") { - if (context.createdNewSession) { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success - } else { - // sign in success - } - } else { - // during step up or second factor auth with email password - } + onHandleEvent: (context) => { + if (context.action === "PASSWORD_RESET_SUCCESSFUL") { + } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { + } else if (context.action === "SUCCESS") { + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success } + } else { + // during step up or second factor auth with email password + } } -}) + }, +}); ThirdParty.init({ - onHandleEvent: (context) => { - if (context.action === "SUCCESS") { - if (context.createdNewSession) { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success - } else { - // sign in success - } - } else { - // during linking a social account to an existing account - } + onHandleEvent: (context) => { + if (context.action === "SUCCESS") { + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success } + } else { + // during linking a social account to an existing account + } } -}) + }, +}); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIEmailPassword.init({ - onHandleEvent: (context) => { - if (context.action === "PASSWORD_RESET_SUCCESSFUL") { - - } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { - - } else if (context.action === "SUCCESS") { - if (context.createdNewSession) { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success - } else { - // sign in success - } - } else { - // during step up or second factor auth with email password - } + onHandleEvent: (context) => { + if (context.action === "PASSWORD_RESET_SUCCESSFUL") { + } else if (context.action === "RESET_PASSWORD_EMAIL_SENT") { + } else if (context.action === "SUCCESS") { + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success } + } else { + // during step up or second factor auth with email password + } } -}) + }, +}); supertokensUIThirdParty.init({ - onHandleEvent: (context) => { - if (context.action === "SUCCESS") { - if (context.createdNewSession) { - let user = context.user; - if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { - // sign up success - } else { - // sign in success - } - } else { - // during linking a social account to an existing account - } + onHandleEvent: (context) => { + if (context.action === "SUCCESS") { + if (context.createdNewSession) { + let user = context.user; + if (context.isNewRecipeUser && context.user.loginMethods.length === 1) { + // sign up success + } else { + // sign in success } + } else { + // during linking a social account to an existing account + } } -}) + }, +}); ``` - - + + - + - + -:::caution -Not applicable since you need to build custom UI anyway. +:::warning[Not applicable since you need to build custom UI anyway.] When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: - + --- @@ -144,315 +128,233 @@ This function calls the backend before any API call. You can use this to change the request properties. - - - - + + + ```tsx import ThirdParty from "supertokens-auth-react/recipe/thirdparty"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; ThirdParty.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "GET_AUTHORISATION_URL") { - - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - // Note: this could either be sign in or sign up. - // we don't know that at the time of the API call - // since all we have is the authorisation code from - // the social provider - } - - // events such as sign out are in the - // session recipe pre API hook (See the info box below) - - return { - requestInit, url - }; + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; + if (action === "GET_AUTHORISATION_URL") { + } else if (action === "THIRD_PARTY_SIGN_IN_UP") { + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider } -}) -EmailPassword.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "EMAIL_EXISTS") { - - } else if (action === "EMAIL_PASSWORD_SIGN_IN") { - - } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - - } else if (action === "SUBMIT_NEW_PASSWORD") { - - } + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - // events such as sign out are in the - // session recipe pre API hook (See the info box below) + return { + requestInit, + url, + }; + }, +}); - return { - requestInit, url - }; +EmailPassword.init({ + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; + if (action === "EMAIL_EXISTS") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { + } else if (action === "EMAIL_PASSWORD_SIGN_UP") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "SUBMIT_NEW_PASSWORD") { } -}) -``` - - - + // events such as sign out are in the + // session recipe pre API hook (See the info box below) -```tsx + return { + requestInit, + url, + }; + }, +}); +``` + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import supertokensUIThirdParty from "supertokens-auth-react-script/recipe/thirdparty"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIThirdParty.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "GET_AUTHORISATION_URL") { - - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - // Note: this could either be sign in or sign up. - // we don't know that at the time of the API call - // since all we have is the authorisation code from - // the social provider - } - - // events such as sign out are in the - // session recipe pre API hook (See the info box below) - - return { - requestInit, url - }; + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; + if (action === "GET_AUTHORISATION_URL") { + } else if (action === "THIRD_PARTY_SIGN_IN_UP") { + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider } -}) - -supertokensUIEmailPassword.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "EMAIL_EXISTS") { - - } else if (action === "EMAIL_PASSWORD_SIGN_IN") { - - } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - } else if (action === "SUBMIT_NEW_PASSWORD") { - - } - - // events such as sign out are in the - // session recipe pre API hook (See the info box below) + return { + requestInit, + url, + }; + }, +}); - return { - requestInit, url - }; +supertokensUIEmailPassword.init({ + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; + let action = context.action; + if (action === "EMAIL_EXISTS") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { + } else if (action === "EMAIL_PASSWORD_SIGN_UP") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "SUBMIT_NEW_PASSWORD") { } -}) -``` - + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - + return { + requestInit, + url, + }; + }, +}); +``` + + - + - + - - - - + + + + ```tsx -import ThirdParty from "supertokens-web-js/recipe/thirdparty" -import EmailPassword from "supertokens-web-js/recipe/emailpassword" +import ThirdParty from "supertokens-web-js/recipe/thirdparty"; +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; EmailPassword.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "EMAIL_EXISTS") { - - } else if (action === "EMAIL_PASSWORD_SIGN_IN") { - - } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - - } else if (action === "SUBMIT_NEW_PASSWORD") { + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; - } + let action = context.action; + if (action === "EMAIL_EXISTS") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { + } else if (action === "EMAIL_PASSWORD_SIGN_UP") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "SUBMIT_NEW_PASSWORD") { + } - // events such as sign out are in the - // session recipe pre API hook (See the info box below) + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - return { - requestInit, url - }; - }, -}) + return { + requestInit, + url, + }; + }, +}); ThirdParty.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "GET_AUTHORISATION_URL") { - - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - // Note: this could either be sign in or sign up. - // we don't know that at the time of the API call - // since all we have is the authorisation code from - // the social provider - } - - // events such as sign out are in the - // session recipe pre API hook (See the info box below) - - return { - requestInit, url - }; - }, -}) -``` - - -Alternatively you could also declare the pre-API hook when calling the function: - - -```tsx -import EmailPassword from "supertokens-web-js/recipe/emailpassword" - -EmailPassword.doesEmailExist({ - email: "...", - options: { - preAPIHook: async (input) => { + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; - let url = input.url - let requestInit = input.requestInit + let action = context.action; + if (action === "GET_AUTHORISATION_URL") { + } else if (action === "THIRD_PARTY_SIGN_IN_UP") { + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider + } - // TODO: add your code here + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - return {url, requestInit}; - }, - } + return { + requestInit, + url, + }; + }, }); ``` - - - - -```tsx -import supertokensThirdParty from "supertokens-web-js-script/recipe/thirdparty"; -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword"; + + +```tsx check=false reason="script-tag installation provides SuperTokens globals at runtime" supertokensEmailPassword.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "EMAIL_EXISTS") { - - } else if (action === "EMAIL_PASSWORD_SIGN_IN") { - - } else if (action === "EMAIL_PASSWORD_SIGN_UP") { - - } else if (action === "SEND_RESET_PASSWORD_EMAIL") { - - } else if (action === "SUBMIT_NEW_PASSWORD") { + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; - } + let action = context.action; + if (action === "EMAIL_EXISTS") { + } else if (action === "EMAIL_PASSWORD_SIGN_IN") { + } else if (action === "EMAIL_PASSWORD_SIGN_UP") { + } else if (action === "SEND_RESET_PASSWORD_EMAIL") { + } else if (action === "SUBMIT_NEW_PASSWORD") { + } - // events such as sign out are in the - // session recipe pre API hook (See the info box below) + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - return { - requestInit, url - }; - }, -}) + return { + requestInit, + url, + }; + }, +}); supertokensThirdParty.init({ - preAPIHook: async (context) => { - let url = context.url; - let requestInit = context.requestInit; - - let action = context.action; - if (action === "GET_AUTHORISATION_URL") { - - } else if (action === "THIRD_PARTY_SIGN_IN_UP") { - // Note: this could either be sign in or sign up. - // we don't know that at the time of the API call - // since all we have is the authorisation code from - // the social provider - } - - // events such as sign out are in the - // session recipe pre API hook (See the info box below) - - return { - requestInit, url - }; - }, -}) -``` - - -Alternatively you could also declare the pre-API hook when calling the function: - + preAPIHook: async (context) => { + let url = context.url; + let requestInit = context.requestInit; -```tsx -import supertokensEmailPassword from "supertokens-web-js-script/recipe/emailpassword" -supertokensEmailPassword.doesEmailExist({ - email: "...", - options: { - preAPIHook: async (input) => { + let action = context.action; + if (action === "GET_AUTHORISATION_URL") { + } else if (action === "THIRD_PARTY_SIGN_IN_UP") { + // Note: this could either be sign in or sign up. + // we don't know that at the time of the API call + // since all we have is the authorisation code from + // the social provider + } - let url = input.url - let requestInit = input.requestInit + // events such as sign out are in the + // session recipe pre API hook (See the info box below) - return {url, requestInit}; - }, - } + return { + requestInit, + url, + }; + }, }); - ``` - - - - - - - - - - - - + + + + + + ```tsx import SuperTokens from "supertokens-react-native"; @@ -479,13 +381,9 @@ SuperTokens.init({ }; }, }); - ``` - - - - - + + ```kotlin import android.app.Application import com.supertokens.session.CustomHeaderProvider @@ -510,11 +408,8 @@ class MainApplication : Application() { } } ``` - - - - - + + ```swift import UIKit import SuperTokensIOS @@ -552,11 +447,8 @@ fileprivate class MyApplicationDelegate: UIResponder, UIApplicationDelegate { } ``` - - - - - + + ```dart import 'package:supertokens_flutter/supertokens.dart'; @@ -575,133 +467,176 @@ void main() { ); } ``` + + + + + + + +
+Alternatively you could also declare the pre-API hook when calling the function: +
+
+
+ + + + + +```tsx +import EmailPassword from "supertokens-web-js/recipe/emailpassword"; -
+EmailPassword.doesEmailExist({ + email: "...", + options: { + preAPIHook: async (input) => { + let url = input.url; + let requestInit = input.requestInit; -
+ // TODO: add your code here -
+ return { url, requestInit }; + }, + }, +}); +``` + + +```tsx check=false reason="script-tag installation provides SuperTokens globals at runtime" +supertokensEmailPassword.doesEmailExist({ + email: "...", + options: { + preAPIHook: async (input) => { + let url = input.url; + let requestInit = input.requestInit; -
+ return { url, requestInit }; + }, + }, +}); +``` + + + + -
+ + + --- ## Redirection callback hook - + Use this function to change where the system redirects the user after certain actions. For example, you can use this to redirect a user to a specific URL post sign in or sign up. If you're embedding the UI components in a popup and wish to disable redirection entirely, return `null`. - - - + + ```tsx import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - appName: "SuperTokens", - apiDomain: "http://localhost:3000", - websiteDomain: "http://localhost:3000" - }, - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - // called on a successful sign in / up. Where should the user go next? - let redirectToPath = context.redirectToPath; - if (redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return redirectToPath; - } - if (context.createdNewUser) { - // user signed up - return "/onboarding" - } else { - // user signed in - return "/dashboard" - } - } else if (context.action === "TO_AUTH") { - // called when the user is not authenticated and needs to be redirected to the auth page. - return "/auth"; + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + // called on a successful sign in / up. Where should the user go next? + let redirectToPath = context.redirectToPath; + if (redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return redirectToPath; + } + if (context.createdNewUser) { + // user signed up + return "/onboarding"; + } else { + // user signed in + return "/dashboard"; + } + } else if (context.action === "TO_AUTH") { + // called when the user is not authenticated and needs to be redirected to the auth page. + return "/auth"; + } + // return undefined to let the default behaviour play out + return undefined; + }, + recipeList: [ + EmailPassword.init({ + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; - }, - recipeList: [ - EmailPassword.init({ - getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - // called when the user clicked on the forgot password button - } - // return undefined to let the default behaviour play out - return undefined; - } - })] + }, + }), + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; supertokensUIInit({ - appInfo: { - appName: "SuperTokens", - apiDomain: "http://localhost:3000", - websiteDomain: "http://localhost:3000" - }, - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS" && context.newSessionCreated) { - // called on a successful sign in / up. Where should the user go next? - let redirectToPath = context.redirectToPath; - if (redirectToPath !== undefined) { - // we are navigating back to where the user was before they authenticated - return redirectToPath; - } - if (context.createdNewUser) { - // user signed up - return "/onboarding" - } else { - // user signed in - return "/dashboard" - } - } else if (context.action === "TO_AUTH") { - // called when the user is not authenticated and needs to be redirected to the auth page. - return "/auth"; + appInfo: { + appName: "SuperTokens", + apiDomain: "http://localhost:3000", + websiteDomain: "http://localhost:3000", + }, + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS" && context.newSessionCreated) { + // called on a successful sign in / up. Where should the user go next? + let redirectToPath = context.redirectToPath; + if (redirectToPath !== undefined) { + // we are navigating back to where the user was before they authenticated + return redirectToPath; + } + if (context.createdNewUser) { + // user signed up + return "/onboarding"; + } else { + // user signed in + return "/dashboard"; + } + } else if (context.action === "TO_AUTH") { + // called when the user is not authenticated and needs to be redirected to the auth page. + return "/auth"; + } + // return undefined to let the default behaviour play out + return undefined; + }, + recipeList: [ + supertokensUIEmailPassword.init({ + getRedirectionURL: async (context) => { + if (context.action === "RESET_PASSWORD") { + // called when the user clicked on the forgot password button } // return undefined to let the default behaviour play out return undefined; - }, - recipeList: [ - supertokensUIEmailPassword.init({ - getRedirectionURL: async (context) => { - if (context.action === "RESET_PASSWORD") { - // called when the user clicked on the forgot password button - } - // return undefined to let the default behaviour play out - return undefined; - } - })] + }, + }), + ], }); ``` - - + + - - + + -:::caution -Not applicable since you need to build custom UI anyway. +:::warning[Not applicable since you need to build custom UI anyway.] When you call the functions from the SDK, or call the API directly, you can run custom logic in your own code. ::: - + diff --git a/docs/references/frontend-sdks/meta.ts b/docs/references/frontend-sdks/meta.ts new file mode 100644 index 0000000000..8f71f4e871 --- /dev/null +++ b/docs/references/frontend-sdks/meta.ts @@ -0,0 +1,7 @@ +import { defineMeta } from "blume"; + +export default defineMeta({ + title: "Frontend SDKs", + icon: "monitor", + order: 40, +}); diff --git a/docs/references/frontend-sdks/prebuilt-ui/_category_.json b/docs/references/frontend-sdks/prebuilt-ui/_category_.json deleted file mode 100644 index fe679ee0ed..0000000000 --- a/docs/references/frontend-sdks/prebuilt-ui/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "User Interface", - "customProps": { - "isMainCategory": true - }, - "collapsed": true, - "position": 3 -} diff --git a/docs/references/frontend-sdks/prebuilt-ui/changing-colours.mdx b/docs/references/frontend-sdks/prebuilt-ui/changing-colours.mdx index 2d33b67188..83258f7c07 100644 --- a/docs/references/frontend-sdks/prebuilt-ui/changing-colours.mdx +++ b/docs/references/frontend-sdks/prebuilt-ui/changing-colours.mdx @@ -1,14 +1,11 @@ --- title: Change colours -sidebar_position: 2 -description: >- - Customize your authentication UI theme by defining CSS variables for color - palettes. -page_type: guide -category: frontend-sdk +description: Customize your authentication UI theme by defining CSS variables for color palettes. +sidebar: + icon: palette + order: 10 --- - ## Overview You can update the default theme with your colors to make it fit with your website. Define a few CSS variables in the `style` property to the `EmailPassword.init` call. @@ -19,26 +16,24 @@ For example, if your website uses a dark theme, here is how you can customize it ## Before you start -:::caution no-title +:::warning This example is relevant only if you use the prebuilt UI components. ::: ## Example - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; @@ -52,26 +47,23 @@ SuperTokens.init({ --palette-textGray: 158, 158, 158; } `, - // highlight-end - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=container] { --palette-background: 51, 51, 51; --palette-inputBackground: 41, 41, 41; @@ -85,16 +77,17 @@ supertokensUIInit({ --palette-textGray: 158, 158, 158; } `, - // highlight-end - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - + + -Prebuilt form UI with custom color palette +Prebuilt form UI with custom color palette -:::important +:::note Changes to the palette apply to all the UI components provided. If you want to change a specific component, please see [this section](changing-style). ::: @@ -120,4 +113,3 @@ Changes to the palette apply to all the UI components provided. If you want to c | `buttonText` | Color of text in main buttons | `255, 255, 255` (white) | | `superTokensBrandingBackground` | Color of SuperTokens branding element | `242, 245, 246` (Alice blue) | | `superTokensBrandingText` | Color of "Powered by SuperTokens" text | `173, 189, 196` (heather grey) | - diff --git a/docs/references/frontend-sdks/prebuilt-ui/changing-style.mdx b/docs/references/frontend-sdks/prebuilt-ui/changing-style.mdx index 6b6a9cb757..6b27a60d43 100644 --- a/docs/references/frontend-sdks/prebuilt-ui/changing-style.mdx +++ b/docs/references/frontend-sdks/prebuilt-ui/changing-style.mdx @@ -1,12 +1,11 @@ --- title: Change styles via CSS -sidebar_position: 3 description: Change UI component styles using CSS. -page_type: guide -category: frontend-sdk +sidebar: + icon: paintbrush + order: 20 --- - ## Overview Updating the CSS allows you to change the UI of the components to meet your needs. @@ -15,7 +14,7 @@ Note that the process can update any HTML tag from within SuperTokens components ## Before you start -:::caution no-title +:::warning This example is relevant only if you use the prebuilt UI components. ::: @@ -25,28 +24,26 @@ This example is relevant only if you use the prebuilt UI components. First, open the website at `/auth`. The Sign-in widget should show up. Use the browser console to find out the class name that you'd like to overwrite. -Inspecting submit button in prebuilt form +Inspecting submit button in prebuilt form -Highlighting attribute for customization +Highlighting attribute for customization Each stylable component contains `data-supertokens` attributes (in this example `data-supertokens="button"`). Let's customize elements with the `button` attribute. The syntax for styling is plain CSS. - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; @@ -54,26 +51,23 @@ SuperTokens.init({ margin: 0 auto; } `, - // highlight-end - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; @@ -81,83 +75,83 @@ supertokensUIInit({ margin: 0 auto; } `, - // highlight-end - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - + + The above results in: -Prebuilt form with custom submit button +Prebuilt form with custom submit button ### Changing fonts By default, SuperTokens uses the `Arial` font. The best way to override this is to add a `font-family` styling to the `container` component in the recipe configuration. - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=container] { font-family: cursive; } `, - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=container] { font-family: cursive; } `, - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - + + ### Using media queries You may want to have different CSS for different `viewports`. This can happen via media queries like this: - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; @@ -171,24 +165,23 @@ SuperTokens.init({ } } `, - recipeList: [ /* ... */], + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - style: ` + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: ` [data-supertokens~=button] { background-color: #252571; border: 0px; @@ -202,11 +195,13 @@ supertokensUIInit({ } } `, - recipeList: [ /* ... */], + recipeList: [ + /* ... */ + ], }); ``` - - + + --- @@ -214,46 +209,46 @@ supertokensUIInit({ These are the screens shown when the user tries to log in or sign up for the application. - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - style: `[data-supertokens~=authPage] { + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: `[data-supertokens~=authPage] { ... }`, - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - style: `[data-supertokens~=authPage] { + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + style: `[data-supertokens~=authPage] { ... }`, - recipeList: [ /* ... */] + recipeList: [ + /* ... */ + ], }); ``` - - + + --- @@ -263,129 +258,108 @@ supertokensUIInit({ This form appears when the user clicks on "forgot password" in the sign in form. - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - enterEmailForm: { - style: ` ... ` - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + enterEmailForm: { + style: ` ... `, + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - enterEmailForm: { - style: ` ... ` - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + resetPasswordUsingTokenFeature: { + enterEmailForm: { + style: ` ... `, + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - + + ### Submit new password form This screen appears when the user clicks the password reset link on their email - to enter their new password - - - + + ```tsx import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; import Session from "supertokens-auth-react/recipe/session"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - EmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - submitNewPasswordForm: { - style: ` ... ` - } - } - // highlight-end - }), - Session.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + EmailPassword.init({ + resetPasswordUsingTokenFeature: { + submitNewPasswordForm: { + style: ` ... `, + }, + }, + }), + Session.init(), + ], }); ``` - - - - -```tsx + + +```tsx check=false reason="pre-built UI globals are provided by the host framework" // this goes in the auth route config of your frontend app (once the pre-built UI script has been loaded) -import {init as supertokensUIInit} from "supertokens-auth-react-script"; -import supertokensUIEmailPassword from "supertokens-auth-react-script/recipe/emailpassword"; -import supertokensUISession from "supertokens-auth-react-script/recipe/session"; supertokensUIInit({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "...", - }, - recipeList: [ - supertokensUIEmailPassword.init({ - // highlight-start - resetPasswordUsingTokenFeature: { - submitNewPasswordForm: { - style: ` ... ` - } - } - // highlight-end - }), - supertokensUISession.init() - ] + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + recipeList: [ + supertokensUIEmailPassword.init({ + resetPasswordUsingTokenFeature: { + submitNewPasswordForm: { + style: ` ... `, + }, + }, + }), + supertokensUISession.init(), + ], }); ``` - - - + + diff --git a/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form.mdx b/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form.mdx index 6f370e9a1a..6d17fbf669 100644 --- a/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form.mdx +++ b/docs/references/frontend-sdks/prebuilt-ui/embed-sign-in-up-form.mdx @@ -1,19 +1,14 @@ --- title: Embed the authentication form in a page -sidebar_position: 4 -description: >- - Embed authentication forms in your page using SuperTokens with redirection and - popup options. -page_type: guide -category: frontend-sdk +description: Embed authentication forms in your page using SuperTokens with redirection and popup options. +sidebar: + icon: panels-top-left + order: 40 --- - - - ## Before you start -:::caution no-title +:::warning This example is relevant only if you use the React SDK with prebuilt UI components. ::: @@ -24,162 +19,136 @@ This example is relevant only if you use the React SDK with prebuilt UI componen The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. Upon a successful login, the user automatically redirects to the return value of `getRedirectionURL` (defaulting to `/`). - - - - - - - -```tsx + + + + +```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui" -// highlight-start -import { AuthPage } from 'supertokens-auth-react/ui'; -// highlight-end -// @ts-ignore +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - } else if (context.action === "SUCCESS" && context.newSessionCreated) { - return "/dashboard"; // defaults to "/" - }; - }, - disableAuthRoute: true, - // highlight-end - recipeList: [ /* ... */], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } + }, + disableAuthRoute: true, + recipeList: [ + /* ... */ + ], }); function MyAuthPage() { - const navigate = useNavigate(); - return ( -
-
- // highlight-next-line - -
-
- ); + const navigate = useNavigate(); + return ( +
+
+ +
+
+ ); } ``` - -
- - - -```tsx + + +```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui" -// highlight-start -import { AuthPage } from 'supertokens-auth-react/ui'; -// highlight-end -// @ts-ignore +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; import { useHistory } from "react-router-dom5"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - } else if (context.action === "SUCCESS" && context.newSessionCreated) { - return "/dashboard"; // defaults to "/" - }; - }, - disableAuthRoute: true, - // highlight-end - recipeList: [ /* ... */], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } + }, + disableAuthRoute: true, + recipeList: [ + /* ... */ + ], }); function MyAuthPage() { - const history = useHistory(); - return ( -
-
- // highlight-next-line - -
-
- ); + const history = useHistory(); + return ( +
+
+ +
+
+ ); } ``` - -
- -
- -
- - -```tsx + + + + +```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui" -// highlight-start -import { AuthPage } from 'supertokens-auth-react/ui'; -// highlight-end -// @ts-ignore +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { AuthPage } from "supertokens-auth-react/ui"; import Header from "./header"; -// @ts-ignore import Footer from "./footer"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "TO_AUTH") { - return "/auth"; // return the path where you are rendering the Auth UI - } else if (context.action === "SUCCESS" && context.newSessionCreated) { - return "/dashboard"; // defaults to "/" - }; - }, - disableAuthRoute: true, - // highlight-end - recipeList: [ /* ... */], + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + getRedirectionURL: async (context) => { + if (context.action === "TO_AUTH") { + return "/auth"; // return the path where you are rendering the Auth UI + } else if (context.action === "SUCCESS" && context.newSessionCreated) { + return "/dashboard"; // defaults to "/" + } + }, + disableAuthRoute: true, + recipeList: [ + /* ... */ + ], }); function MyAuthPage() { - return ( -
-
- // highlight-next-line - -
-
- ) + return ( +
+
+ +
+
+ ); } ``` -
-
+ + In the above code snippet: @@ -188,8 +157,7 @@ In the above code snippet: Feel free to customize the redirection URLs as needed. -:::note -When the user visits the `/auth` page, they see the SignIn UI by default. To render the SignUp UI, append `show=signup` as a query parameter to the URL, like`/auth?show=signup`. +:::note[When the user visits the `/auth` page, they see the SignIn UI by default. To render the SignUp UI, append `show=signup` as a query parameter to the URL, like`/auth?show=signup`.] ::: --- @@ -198,225 +166,190 @@ When the user visits the `/auth` page, they see the SignIn UI by default. To ren The following example shows the scenario where you have a dedicated route, such as `/auth`, for rendering the Auth Widget. However, upon a successful login, the user sees a logged in UI instead of getting redirected. - - - - - - - -```tsx + + + + +```tsx check=false reason="component excerpt imports application-local header and footer components" import React from "react"; import SuperTokens from "supertokens-auth-react"; import EmailPassword from "supertokens-auth-react/recipe/emailpassword"; -// highlight-start -import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui" -// highlight-start -import { AuthPage } from 'supertokens-auth-react/ui'; -// highlight-end +import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui"; +import { AuthPage } from "supertokens-auth-react/ui"; import Session from "supertokens-auth-react/recipe/session"; -// @ts-ignore import Header from "./header"; -// @ts-ignore import Footer from "./footer"; import { useNavigate } from "react-router-dom"; SuperTokens.init({ - appInfo: { - apiDomain: "...", - appName: "...", - websiteDomain: "..." - }, - disableAuthRoute: true, - recipeList: [ /* ... */], - // highlight-start - getRedirectionURL: async (context) => { - if (context.action === "SUCCESS") { - return null; // this will not navigate the user away after successful login - } - }, - // highlight-end + appInfo: { + apiDomain: "...", + appName: "...", + websiteDomain: "...", + }, + disableAuthRoute: true, + recipeList: [ + /* ... */ + ], + getRedirectionURL: async (context) => { + if (context.action === "SUCCESS") { + return null; // this will not navigate the user away after successful login + } + }, }); -// highlight-start function LandingPage() { - let sessionContext = Session.useSessionContext(); - const navigate = useNavigate(); - if (sessionContext.loading) { - return null; - } - if (sessionContext.doesSessionExist) { - // We wrap this with so that - // all claims are validated before showing the logged in UI. - // For example, if email verification is switched on, and - // the user's email is not verified, then - // will redirect to the email verification page. - return ( - -
- You are logged in! -